|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
IntroductionFrom the time I started playing with HTML, I was annoyed by the lack of a horizontal scrollbar on the The first problem that came to light was that if you used your arrow keys to cursor down the list, you could disappear off the bottom of the And then came the move to ASP.NET. I wanted to encapsulate my solution in a server control, but I figured that as long as I was going to do that, I might as well correct three of the more annoying aspects of Microsoft's
Combining ListBox and DropDownListThe Microsoft As I've done before, I used Lutz Roeder's Reflector to grab the code that makes up the
Public Enum XListType
ListBox = 0
DropDownList = 1
End Enum 'XListType
I created a property in <DefaultValue(0), Category("Behavior"), Description("XList_XListType")> _
Public Overridable Property XListType() As XListType
Get
Dim obj1 As Object = Me.ViewState("XListType")
If (Not obj1 Is Nothing) Then
Return CType(obj1, XListType)
End If
Return XListType.ListBox
End Get
Set(ByVal value As XListType)
If ((value < XListType.ListBox) OrElse (value > _
XListType.DropDownList)) Then
Throw New ArgumentOutOfRangeException("value")
End If
Me.ViewState("XListType") = value
End Set
End Property 'XListType
You can look at If Me.XListType = XListType.ListBox Then
writer.AddAttribute(HtmlTextWriterAttribute.Size, _
Me.Rows.ToString(NumberFormatInfo.InvariantInfo))
If (Me.SelectionMode = ListSelectionMode.Multiple) Then
writer.AddAttribute(HtmlTextWriterAttribute.Multiple, "multiple")
End If
End If
As you can see, I simply wrapped this piece of code in a conditional statement, so that it only runs if the There were a few other stumbling blocks. Some of the original code references methods in other classes which are marked I really didn't want to have to recreate the The Me.Page.RegisterPostBackScript()
I replaced it with this: Dim methodInfo As methodInfo = _
Me.Page.GetType.GetMethod("RegisterPostBackScript", _
BindingFlags.Instance Or BindingFlags.NonPublic)
If Not (methodInfo Is Nothing) Then
methodInfo.Invoke(Me.Page, New Object() {})
End If
When you're working with custom controls, you may find yourself running into this problem frequently. This is the way around it. Also, the Microsoft code used the For example, in the Dim collection1 As ICollection = _
TryCast(enumerable1, ICollection)
I replaced it with this: Dim collection1 As ICollection
If TypeOf enumerable1 Is ICollection Then
collection1 = CType(enumerable1, ICollection)
Else
collection1 = Nothing
End If
Because that's essentially what That's about all I did to combine the two controls. As I said, you can find it as Adding <optgroup> SupportIt isn't actually that big a deal that First, though, I had to decide how I was going to do this. In the Luckily, I regained my sanity before I did that, and decided merely to add an <DefaultValue("")> _
Public Property OptGroup() As String
Get
Return Me._optGroup
End Get
Set(ByVal value As String)
Me._optGroup = value
If Me.IsTrackingViewState Then
Me._misc.Set(4, True)
End If
End Set
End Property 'OptGroup
You may wonder what this line is: Me._misc.Set(4, True)
It seems that Microsoft's coders decided to put a global Private Const _SELECTED As Integer = 0
Private Const _MARKED As Integer = 1
Private Const _TEXTISDIRTY As Integer = 2
Private Const _VALUEISDIRTY As Integer = 3
For some reason, they chose not to use these constants, instead just using their numeric values, but it was helpful in letting me know what the items in Private Const _OPTGROUPISDIRTY As Integer = 4
So when the I also added some custom state management code for In 'render optgroups if they're enabled
If Me.EnableOptGroups Then
Dim sPrevOptGroup As String
Dim sOptGroup As String = item1.OptGroup
'if the optgroup has changed, unless it's the first
'optgroup, end the previous optgroup
If Not sOptGroup = sPrevOptGroup And Not num2 = 0 Then
writer.WriteEndTag("optgroup")
writer.WriteLine()
End If
'if it's the first optgroup, or if the optgroup
'has changed, start a new optgroup
If Not sOptGroup = sPrevOptGroup Or num2 = 0 Then
writer.WriteBeginTag("optgroup")
writer.WriteAttribute("label", sOptGroup)
writer.Write(">"c)
writer.WriteLine()
sPrevOptGroup = sOptGroup
End If
End If
When this was done, the <Description("XList_DataOptGroupField"), _
Category("Data"), DefaultValue("")> _
Public Overridable Property DataOptGroupField() As String
Get
Dim obj1 As Object = Me.ViewState("DataOptGroupField")
If (Not obj1 Is Nothing) Then
Return CType(obj1, String)
End If
Return String.Empty
End Get
Set(ByVal value As String)
Me.ViewState("DataOptGroupField") = value
End Set
End Property 'DataOptGroupField
<Description("XList_DataOptGroupFormatString"), _
DefaultValue(""), Category("Data")> _
Public Overridable Property DataOptGroupFormatString() As String
Get
Dim obj1 As Object = Me.ViewState("DataOptGroupFormatString")
If (Not obj1 Is Nothing) Then
Return CType(obj1, String)
End If
Return String.Empty
End Get
Set(ByVal value As String)
Me.ViewState("DataOptGroupFormatString") = value
End Set
End Property 'DataOptGroupFormatString
I modified the various methods involved with the databinding to include these properties. You can take a look at the source code to see how that was done. Oh, Where, Oh, Where, Have My Attributes Gone...?I have no idea why Microsoft decided not to support So I decided to workaround them. You'd think that the <Browsable(False), _
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)> _
Public ReadOnly Property Attributes() As System.Web.UI.AttributeCollection
Get
If (Me._attributes Is Nothing) Then
Me._attributes = New _
System.Web.UI.AttributeCollection(New StateBag(True))
End If
Return Me._attributes
End Get
End Property 'Attributes
After all, what's the point of the First, I added a new global constant and changed Private Const _ATTRIBUTESISDIRTY As Integer = 5
Then I added this line to the Me._misc.Set(5, True)
After that, it was simply a matter of modifying Friend Sub LoadViewState(ByVal state As Object)
Dim arrState As Object() = CType(state, Object())
If (Not arrState(0) Is Nothing) Then
If TypeOf arrState(0) Is Pair Then
Dim pair1 As Pair = CType(arrState(0), Pair)
If (Not pair1.First Is Nothing) Then
Me.Text = CType(pair1.First, String)
End If
Me.Value = CType(pair1.Second, String)
Else
Me.Text = CType(arrState(0), String)
End If
End If
'custom state management for OptGroup
If Not arrState(1) Is Nothing Then
Me.OptGroup = CType(arrState(1), String)
End If
'custom state management for Attributes
If Not arrState(2) Is Nothing Then
If TypeOf arrState(2) Is Pair() Then
Dim colAttributes As Pair() = CType(arrState(2), Pair())
For i As Integer = 0 To colAttributes.Length - 1
Me.Attributes.Add(colAttributes(i).First.ToString, _
colAttributes(i).Second.ToString)
Next i
End If
End If
End Sub 'LoadViewState
I loaded the Friend Function SaveViewState() As Object
Dim arrState(2) As Object
If (Me._misc.Get(2) AndAlso Me._misc.Get(3)) Then
arrState(0) = New Pair(Me.Text, Me.Value)
ElseIf Me._misc.Get(2) Then
arrState(0) = Me.Text
ElseIf Me._misc.Get(3) Then
arrState(0) = New Pair(Nothing, Me.Value)
Else
arrState(0) = Nothing
End If
'custom state management for OptGroup
arrState(1) = Me.OptGroup
''custom state management for Attributes
If Me.Attributes.Count > 0 Then
ReDim _attributes2(Me.Attributes.Count - 1)
Dim i As Integer = 0
Dim keys As IEnumerator = Me.Attributes.Keys.GetEnumerator
Dim key As String
While keys.MoveNext()
key = CType(keys.Current, String)
_attributes2(i) = New Pair(key, Me.Attributes.Item(key))
i += 1
End While
arrState(2) = _attributes2
End If
Return arrState
End Function 'SaveViewState
I copied the The changes necessary for Horizontal ScrollingThe previous changes are all cross-browser compatible. This one isn't. The <Category("Appearance"), DefaultValue(False), Description("XList_EnableHScroll")> _
Public Overridable Property EnableHScroll() As Boolean
Get
Dim obj1 As Object = Me.ViewState("EnableHScroll")
If (Not obj1 Is Nothing) Then
Return CType(obj1, Boolean)
End If
Return False
End Get
Set(ByVal value As Boolean)
Me.ViewState("EnableHScroll") = value
End Set
End Property 'EnableHScroll
The With our scrolling If Me.EnableHScroll Then
If Me.Width.IsEmpty Then Me.Width = Unit.Pixel(100)
If Me.Height.IsEmpty Then Me.Height = Unit.Pixel(100)
End If
And we override the <Browsable(True), _
DesignerSerializationVisibility(DesignerSerializationVisibility.Content)> _
Public Overrides Property Width() As Unit
Get
If Me.EnableHScroll AndAlso MyBase.Width.IsEmpty Then
Return Unit.Pixel(100)
End If
Return MyBase.Width
End Get
Set(ByVal Value As Unit)
MyBase.Width = Value
End Set
End Property 'Width
Putting the Public Overrides Sub RenderEndTag(ByVal writer _
As System.Web.UI.HtmlTextWriter)
MyBase.RenderEndTag(writer)
If Me.EnableHScroll Then
writer.RenderEndTag()
End If
End Sub 'RenderEndTag
In addition to the standard code I used to modify MyBase.Attributes.Add("onchange", "javascript:XList_ShowOption(this);")
MyBase.Attributes.Add("onresize", _
"javascript:XList_ResizeSelect(this);XList_ShowOption(this);")
These events trigger one or both of the functions that make the control work. function XList_ResizeSelect(objSelect){
//check to see if the object is visible
if (objSelect.offsetHeight == 0){
return;
}
//remove the onresize event so that it doesn't loop forever
objSelect.onresize = null;
//make sure it's a listbox and not a dropdown
objSelect.size = objSelect.options.length < 2 ? 2 : _
objSelect.options.length;
if (objSelect.offsetHeight < _
objSelect.parentElement.offsetHeight - scrollbarWidth){
objSelect.style.height = _
objSelect.parentElement.offsetHeight - scrollbarWidth / 2;
}
objSelect.style.width = "";
if (objSelect.offsetWidth < objSelect.parentElement.offsetWidth_
- scrollbarWidth){
objSelect.style.width = (objSelect.parentElement.offsetWidth_
- scrollbarWidth) + "px";
} else {
objSelect.style.width = "auto";
}
}
A utility function calculates the width of a scrollbar when the page loads. I gleaned that useful technique from Scott Isaac's article on DHTML scrollbars. I used the
Here's the code that makes the function XList_ShowOption(objSelect){
idx = objSelect.selectedIndex
if (idx == -1){
return;
}
if (objSelect.length == 0){
return;
}
objDiv = objSelect.parentElement;
HeightOfSelect = objSelect.clientHeight;
OptionsInSelect = objSelect.options.length;
HeightOfOption = HeightOfSelect / OptionsInSelect;
HeightOfDiv = objDiv.clientHeight;
OptionsInDiv = HeightOfDiv / HeightOfOption;
OptionTopFromTopOfSelect = HeightOfOption * idx;
OptionTopFromTopOfDiv = OptionTopFromTopOfSelect - objDiv.scrollTop;
OptionBottomFromBottomOfDiv = HeightOfDiv - _
OptionTopFromTopOfDiv - HeightOfOption;
if (OptionTopFromTopOfDiv < 0) {
objDiv.scrollTop = OptionTopFromTopOfSelect;
} else if (OptionBottomFromBottomOfDiv < 0 && _
OptionBottomFromBottomOfDiv > 0 - HeightOfOption) {
objDiv.scrollTop = objDiv.scrollTop + HeightOfOption;
} else if (OptionBottomFromBottomOfDiv < 0) {
objDiv.scrollTop = OptionTopFromTopOfSelect;
}
}
These two functions, along with the utility that figures out the width of a scrollbar, get written to the page once, regardless of how many instances of When the page loads, the In the design environment, however, scripts don't run. So I modified the Public Overrides Function GetDesignTimeHtml() As String
Dim collection1 As XListItemCollection = Me._xList.Items
If (collection1.Count > 0) Then
'Return MyBase.GetDesignTimeHtml
Return GetDesignTimeResize()
End If
If Me.IsDataBound Then
collection1.Add("bound")
Else
collection1.Add("unbound")
End If
'Dim text1 As String = MyBase.GetDesignTimeHtml
Dim text1 As String = GetDesignTimeResize()
collection1.Clear()
Return text1
End Function 'GetDesignTimeHtml
Public Overridable Function GetDesignTimeResize() As String
Dim _xList As XList = CType(Component, XList)
If _xList.EnableHScroll Then
Dim str As String = MyBase.GetDesignTimeHtml
str = Replace(str, "<select", _
"<select style=width:" & _xList.Width.ToString)
Dim _itemCount As Integer = _xList.Items.Count
Dim _selectTagEnd As Integer = InStr(str, ">")
_selectTagEnd = InStr(_selectTagEnd + 1, str, ">")
Dim _sizeAttribute As Integer = InStr(str, " size=")
If _sizeAttribute > 0 And _sizeAttribute < _selectTagEnd Then
str = Replace(str, "size=""4""", "size=""" & _
IIf(_itemCount < 2, 2, _itemCount).ToString & """")
Else
str = Replace(str, "<select", "<select size=""" _
& IIf(_itemCount < 2, 2, _itemCount).ToString & """")
End If
Return str
Else
Return MyBase.GetDesignTimeHtml
End If
End Function 'GetDesignTimeResize
About the only thing I found left to do was to add a few lines at the end of ConclusionThat's pretty much it. The control works in three modes:
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||