Introduction
One of the great features that the .NET framework has is the developer's ability to control the way the types are designed. While all the basic types are supplied with their own default editors, some of the types are used for variety of purposes, and a generalized editor is not always convenient enough.
Storing a filename string, either with full or relative path is quite a frequent need. Oddly, FileNameEditor
class supplies only generic non-modifiable file name dialog. To assign your own title or filter, one must inherit it (oddly, no Filter or Title property is supplied). But it is not a big deal as we'll build one in a matter of minutes.
Background: Crash Course on Type Editors
Type editors are invoked when altering an object from a GUI like PropertyGrid
control, or its numerous clones. Every row contains a name of a property and its value, which can or cannot be edited. If a type editor is associated with the property, usually an ellipsis or a drop-down button appears in the value cell. Same type editors though leave the cell as it is. .NET 1.1 has 3 kinds of editors (or "styles", by MS terms):
- Modal editor which is invoked by pressing the ellipsis button. It is intended to show a modal form, however, it is up to its developer how exactly it works. In the
UITypeEditorEditStyle
enumeration, this style is associated with UITypeEditorEditStyle.Modal
.
- Drop-down editor. It is expected that the developer will create a control which will be shown just below the cell. The width of the control is automatically justified to match the width of the value cell. In the
UITypeEditorEditStyle
enumeration, this style is associated with UITypeEditorEditStyle.DropDown
.
- Free input editor. Normally, no interactive GUI should be used here. In the
UITypeEditorEditStyle
enumeration, this style is associated with UITypeEditorEditStyle.None
.
To write a fully functional type editor, two methods must be overridden: GetEditStyle
and EditValue
. The former determines the style of the editor, while the latter is invoked when the GUI is activated by user. The returned value is assigned to the property being edited.
Let's Code!
Now, when we know how it works, let's get started. Since it is natural for the filename editor to open a file dialog which is a modal window, our editor has Modal
style:
Public Overloads Overrides Function GetEditStyle(ByVal context As _
ITypeDescriptorContext) As UITypeEditorEditStyle
If Not context Is Nothing AndAlso Not context.Instance Is Nothing Then
Return UITypeEditorEditStyle.Modal
End If
Return UITypeEditorEditStyle.None
End Function
The next method to implement will be EditValue
. For now, we'll just create an OpenFileDialog
and show it to the user:
Public Overloads Overrides Function EditValue( _
ByVal context As ITypeDescriptorContext, _
ByVal provider As System.IServiceProvider, _
ByVal value As [Object]) As [Object]
If context Is Nothing OrElse provider Is Nothing _
OrElse context.Instance Is Nothing Then
Return MyBase.EditValue(provider, value)
End If
Dim fileDlg As New OpenFileDialog
fileDlg.FileName = value
fileDlg.Title = "Select " & context.PropertyDescriptor.DisplayName
fileDlg.Filter = "All Files (*.*)|*.*"
If fileDlg.ShowDialog() = DialogResult.OK Then
value = fileDlg.FileName
End If
fileDlg.Dispose()
Return value
End Function
That's it! Ready to go.
Using the code
To use the class, bind it to your property like this:
<Editor(GetType(UIFilenameEditor), GetType(UITypeEditor))> _
Property MyFilename() As String
Bells and Whistles
Isn't it all too simplistic? I mean, browsing through all the files, always in the "open file" mode... Certainly, there is room for improvement. The problem is, there is no way to pass these parameters from the class to the editor, because the Editor attribute receives only the type rather than instance. There are ways to solve this problem though.
One, less elegant, is using the editor's Context.Instance
to obtain the reference to the object being edited, and then setting the dialog's specifications from the object's members (possibly outlined by a special interface). This is not good - we have to mix the design layer with the class.
The other way is making use of additional attributes. The user might use or not use these attributes. This is the preferable way of passing the parameters to the editor.
Writing attribute classes is very simple. There are only two requirements:
- Your class must inherit the
Attribute
class.
- The name of your class must end with the string "Attribute". (Yes, I don't understand why either.)
What attributes do we need? It would be neat to be able to specify the extensions to look for (the filter), and whether to show save file dialog rather than open file dialog.
The simple one first.
<AttributeUsage(AttributeTargets.Property)> _
Public Class SaveFileAttribute
Inherits Attribute
End Class
That's it! Note the AttributeUsage
attribute (hmm...). It is not compulsory, but it is good for the sanity check: specifying where your attribute class is valid.
To use your attribute, bind it to your property like this:
<Editor(GetType(UIFilenameEditor), GetType(UITypeEditor)), SaveFile> _
Property MyFilename() As String
Now, we'll create the filter attribute. As we need it to hold a parameter, we also need to provide a respective data member and a parameter, like this:
<AttributeUsage(AttributeTargets.Property)> _
Public Class FileDialogFilterAttribute
Inherits Attribute
Private _filter As String
Public ReadOnly Property Filter() As String
Get
Return Me._filter
End Get
End Property
Public Sub New(ByVal filter As String)
MyBase.New()
Me._filter = filter
End Sub
End Class
Again, not very sophisticated. As you can see, the constructor is overridden - this is the way to pass the data to an attribute.
To use your attribute, bind it to your property like this:
<Editor(GetType(UIFilenameEditor), GetType(UITypeEditor)), _
SaveFile, _
FileDialogFilter("Text files (*.txt)|*.txt|All files (*.*)|*.*")> _
Property MyFilenameWhichIWantToUseForATextFileToSave() As String
We've done with the attributes, but now we must make use of them. For this, we'll modify the EditValue
method in the UIFilenameEditor
class:
Public Overloads Overrides Function EditValue( _
ByVal context As ITypeDescriptorContext, _
ByVal provider As System.IServiceProvider, _
ByVal value As [Object]) As [Object]
If context Is Nothing OrElse provider Is Nothing _
OrElse context.Instance Is Nothing Then
Return MyBase.EditValue(provider, value)
End If
Dim fileDlg As FileDialog
If context.PropertyDescriptor.Attributes(GetType(SaveFileAttribute))_
Is Nothing Then
fileDlg = New OpenFileDialog
Else
fileDlg = New SaveFileDialog
End If
fileDlg.Title = "Select " & context.PropertyDescriptor.DisplayName
fileDlg.FileName = value
Dim filterAtt As FileDialogFilterAttribute = _
context.PropertyDescriptor.Attributes(GetType(FileDialogFilterAttribute))
If Not filterAtt Is Nothing Then fileDlg.Filter = filterAtt.Filter
If fileDlg.ShowDialog() = DialogResult.OK Then
value = fileDlg.FileName
End If
fileDlg.Dispose()
Return value
End Function
As you can guess, these context.PropertyDescriptor.Attributes(GetType(MyAttribute))
clauses fetch the attribute of the specified type, if there's any. It is also the way to check if the attribute is specified.
That's it. Enjoy! Next time - more handy type editors.