1

In C# or VB.NET, under .NET Framework 4.x in Windows Forms, I would like to write a universal function to invoke the default UI Editor at runtime for the specified control property type.

Example (incomplete code):

public T EditValue<T>(Component component, string propertyName, T value) {

    PropertyDescriptor propDescriptor = 
        TypeDescriptor.GetProperties(component)[propertyName];

    UITypeEditor editor = 
        (UITypeEditor)propDescriptor.GetEditor(typeof(UITypeEditor));

    IWindowsFormsEditorService serviceProvider = ??????;

    object result = editor.EditValue(serviceProvider, serviceProvider, value);
    return (T)result;
}

( Of course the function definition can also have the ExtensionAttribute specified to simplify the function calls. )

Example usage would be like this to edit the Control.Font property:

TextBox ctrl = this.TextBox1;
Font value = EditValue(ctrl, nameof(ctrl.Font), ctrl.Font);

ctrl.Font = value;

Or to edit the items in a ListBox:

ListBox ctrl = this.ListBox1;
ListBox.ObjectCollection value = EditValue(ctrl, nameof(ctrl.Items), ctrl.Items);

I need help to figure whether this is or not the correct approach to implement this kind of functionality (maybe all this can be done easier through Reflection or other means?), and also help to obtain the default IServiceProvider / IWindowsFormsEditorService instance to be able edit the control that I pass to the function of the code above.


I researched and discovered this answer which demonstrates how to define a new class that implements IServiceProvider / IWindowsFormsEditorService interfaces:

https://stackoverflow.com/a/3816585/1248295

So in the code above I can replace this line:

IWindowsFormsEditorService serviceProvider = ??????;

For this:

RuntimeServiceProvider serviceProvider = new RuntimeServiceProvider();

And it works:

enter image description here

But what I'm asking if there is already a class defined within the form, component or control type that already implements IServiceProvider / IWindowsFormsEditorService for this purpose so I could instantiate it or retrieve it through Reflection to instantiate it and so simplify my code to avoid writing more code just to define the RuntimeServiceProvider class.

I'm asking if things can be simplified without the requirement to define a custom class like RuntimeServiceProvider to implement IWindowsFormsEditorService.

ElektroStudios
  • 19,105
  • 33
  • 200
  • 417
  • 1
    I'm sure there is more this, but that seems to be a horribly complex way to call the font editor dialogue when you can just drop the control on the form designer or create an instance via code. – Hursey Oct 20 '22 at 21:24
  • 1
    The PropertyGrid, to select the UITypeEditor of the selected Property, uses a custom IServiceProvider object ([PropertyGridServiceProvider](https://referencesource.microsoft.com/#System.Windows.Forms/winforms/Managed/System/WinForms/PropertyGrid.cs,5230)), when the ISite of a Component can get the IDesignerHost service, otherwise a custom Control ([PropertyGridView](https://referencesource.microsoft.com/#System.Windows.Forms/winforms/Managed/System/WinForms/PropertyGridInternal/PropertyGridView.cs,35)) that acts as both `IWindowsFormsEditorService` and `IServiceProvider` – Jimi Oct 20 '22 at 21:47
  • 1
    Hence, a simple class that implements `IServiceProvider` and generates a *stub* `IWindowsFormsEditorService` is probably the very minimum. I don't think you need acrobatic actions to get a default `IServiceProvider`, created on the fly somewhere else – Jimi Oct 20 '22 at 21:50
  • @Hursey Yes, you can always instantiate the **FontDialog** class via code, but to do that you also must be aware of the object type (Font) for which to work with. The code that I wrote in the main post I called it "universal function" because being unaware of the object type (a generic type) it would link it to the right value editor at runtime, like in the usage examples that I shared. – ElektroStudios Oct 20 '22 at 22:02
  • @Jimi The IDesignerHost + IDesigner instances makes it a lot simpler when the control is in design-mode. But the "Component.Site" property is empty in the scenario I am. About the PropertyGridView class, I got it through Reflection but it seems does not have a parameterless constructor. Is there any way to use it to act as both IWindowsFormsEditorService and IServiceProvider?. Thanks to you and Hursey for the help. – ElektroStudios Oct 20 '22 at 22:18
  • Perhaps I'm being too stubborn to insist on doing acrobatic things to try simplify the code a bit, but for example if the **PropertyGridView** class could have instantiated it, the solution would have taken two lines of code compared to a hundred lines to define a simple class that implements **IWindowsFormsEditorService** and **IServiceProvider**. Furthermore I would like to use any built-in class in the .NET Framework library that implements these interfaces because for sure it will be more robust than that simple class. – ElektroStudios Oct 20 '22 at 22:30
  • 1
    Well, you *could* initialize it as `new PropertyGridView(null, null);` and set its ServiceProvider Property when an ISite is available (as the PropertyGrid does), but that's an internal class, the second `null` is the `Owner`, the PropertyGrid itself. I really don't think you want to mess with this class -- The *generic* IServiceProvider is also an internal class, `EditorServiceContext`. I don't think its code is public (unless you're a MVP :), but I also think you can find it around – Jimi Oct 20 '22 at 22:36
  • Well, it doesn't matter, Reza has [already posted it](https://stackoverflow.com/a/43827054/7444103), a while ago – Jimi Oct 20 '22 at 23:09
  • But I only can make that work when the control is in design-mode, when the Site property is not null. I tried by instantiating a new ComponentDesigner class and using the ComponentDesigner.Initialize method to see if that makes the trick when requiring a IDesigner instance ( my ComponentDesigner class instance ) by the EditValue method of EditorServiceContext, but it does not. – ElektroStudios Oct 20 '22 at 23:16

1 Answers1

0

Without being able to find other approach more simplified, I'm being forced to define a class to implement ITypeDescriptorContext, IServiceProvider and IWindowsFormsEditorService. In this case I define three classes just because I prefer separate implementations that could be more useful to use alone in the future, but all can be defined in a single class like @Reza's C# examples.

This will not be the accepted answer, but it is a valid solution for lack of something more simplified. And it is written in VB.NET.

Here we go:

ServiceProvider.vb

Imports System.Windows.Forms.Design

#Region " ServiceProvider "

Namespace DevCase.Core.Design.Services

    ''' <summary>Provides a simple implementation of <see cref="IServiceProvider"/> 
    ''' that provides a mechanism for retrieving a service object;
    ''' that is, an object that provides custom support to other objects.</summary>
    ''' <seealso cref="IServiceProvider"/>
    Public Class ServiceProvider : Implements IServiceProvider

#Region " Constructors "

        ''' <summary>Initializes a new instance of the <see cref="ServiceProvider"/> class.</summary>
        Public Sub New()
            MyBase.New()
        End Sub

#End Region

#Region " IServiceProvider Implementation "

        ''' <summary>Gets the service object of the specified type.</summary>
        ''' <param name="serviceType">An object that specifies the type of service object to get.</param>
        ''' <returns>A service object of type <paramref name="serviceType"/>. 
        ''' -or- null if there is no service object of type <paramref name="serviceType"/>.</returns>
        Public Overridable Function GetService(serviceType As Type) As Object Implements IServiceProvider.GetService
            If serviceType Is GetType(IWindowsFormsEditorService) Then
                Return New DevCase.Core.Design.Services.WindowsFormsEditorService()
            End If

            Return Nothing
        End Function

#End Region

    End Class

End Namespace

#End Region

WindowsFormsEditorService.vb

Imports System.Drawing.Design
Imports System.Windows.Forms
Imports System.Windows.Forms.Design

#Region " WindowsFormsEditorService "

Namespace DevCase.Core.Design.Services

    ''' <summary> Provides a simple implementation of <see cref="IWindowsFormsEditorService"/> interface 
    ''' for a <see cref="UITypeEditor"/> to display Windows Forms or to display a control 
    ''' in a drop-down area from a property grid control in design mode or runtime mode.</summary>
    ''' <seealso cref="IWindowsFormsEditorService"/>
    Public Class WindowsFormsEditorService : Implements IWindowsFormsEditorService

#Region " Constructors "

        ''' <summary>Initializes a new instance of the <see cref="WindowsFormsEditorService"/> class.</summary>
        Public Sub New()
            MyBase.New()
        End Sub

#End Region

#Region " IWindowsFormsEditorService Implementation "

        ''' <summary>Closes any previously opened drop down control area.</summary>
        Protected Overridable Sub CloseDropDown() Implements IWindowsFormsEditorService.CloseDropDown
        End Sub

        ''' <summary>Displays the specified control in a drop down area 
        ''' below a value field of the property grid that provides this service.</summary>
        ''' <param name="control">The drop down list  <see cref="System.Windows.Forms.Control"/> to open.</param>
        Protected Overridable Sub DropDownControl(control As Control) Implements IWindowsFormsEditorService.DropDownControl
        End Sub

        ''' <summary>Shows the specified <see cref="Form"/>.</summary>
        ''' <param name="dialog">The <see cref="Form"/> to display.</param>
        ''' <returns>A <see cref="DialogResult"/> indicating the result code returned by the <see cref="Form"/>.</returns>
        Public Overridable Function ShowDialog(dialog As Form) As DialogResult Implements IWindowsFormsEditorService.ShowDialog
            Dim result As DialogResult = dialog.ShowDialog()
            Return result
        End Function

#End Region

    End Class

End Namespace

#End Region

TypeDescriptorContext.vb

Imports System.ComponentModel
Imports System.ComponentModel.Design

#Region " TypeDescriptorContext "

Namespace DevCase.Core.Design.Services

    ''' <summary>Provides a simple implementation of <see cref="ITypeDescriptorContext"/> interface
    ''' that defines contextual information about a <see cref="Component"/>, 
    ''' such as its <see cref="IContainer"/> and <see cref="ComponentModel.PropertyDescriptor"/>.</summary>
    ''' <seealso cref="ITypeDescriptorContext"/>
    Public Class TypeDescriptorContext : Implements ITypeDescriptorContext

#Region " Constructors "

        ''' <summary>Initializes a new instance of the <see cref="TypeDescriptorContext"/> class.</summary>
        ''' <param name="component">The <see cref="Component"/> that will be associated with this <see cref="TypeDescriptor"/></param>
        ''' <param name="[property]">The <see cref="ComponentModel.PropertyDescriptor"/> that will be associated with this <see cref="TypeDescriptorContext"/>.</param>
        Public Sub New(component As Component, [property] As PropertyDescriptor)
            MyBase.New()
            Me.Instance = component
            Me.PropertyDescriptor = [property]
        End Sub

        ''' <summary>Initializes a new instance of the <see cref="TypeDescriptorContext"/> class.</summary>
        ''' <param name="component">The <see cref="Component"/> that will be associated with this <see cref="TypeDescriptor"/></param>
        Public Sub New(component As Component)
            Me.New(component, [property]:=Nothing)
        End Sub

        ''' <summary>Initializes a new instance of the <see cref="TypeDescriptorContext"/> class.</summary>
        Public Sub New()
            Me.New(component:=Nothing, [property]:=Nothing)
        End Sub

#End Region

#Region " ITypeDescriptorContext Implementation "

        ''' <summary>Gets the <see cref="Component"/> that is associated with this <see cref="TypeDescriptor"/>.</summary>
        Public ReadOnly Property Instance As Object Implements ITypeDescriptorContext.Instance

        ''' <summary>Gets the container that is associated with this <see cref="TypeDescriptor"/>.</summary>
        Public ReadOnly Property Container As IContainer Implements ITypeDescriptorContext.Container
            Get
                Return DirectCast(Me.Instance, Component).Container
            End Get
        End Property

        ''' <summary>Gets the <see cref="ComponentModel.PropertyDescriptor"/> that is associated with this <see cref="TypeDescriptor"/>.</summary>
        Public ReadOnly Property PropertyDescriptor As PropertyDescriptor Implements ITypeDescriptorContext.PropertyDescriptor

        ''' <summary>Raises the<see cref="IComponentChangeService.ComponentChanged"/> event.</summary>
        Public Overridable Sub OnComponentChanged() Implements ITypeDescriptorContext.OnComponentChanged
        End Sub

        ''' <summary>Raises the<see cref="IComponentChangeService.ComponentChanging"/> event. </summary>
        Public Overridable Function OnComponentChanging() As Boolean Implements ITypeDescriptorContext.OnComponentChanging
            Return True ' True to keep changes; otherwise, False.
        End Function

#End Region

#Region " IServiceProvider Implementation "

        ''' <summary>Gets the service object of the specified type.</summary>
        ''' <param name="serviceType">An object that specifies the type of service object to get. </param>
        ''' <returns> A service object of type <paramref name="serviceType"/>. 
        ''' -or- null if there is no service object of type <paramref name="serviceType"/>.</returns>
        Public Overridable Function GetService(serviceType As Type) As Object Implements IServiceProvider.GetService
            Dim serviceProvider As New DevCase.Core.Design.Services.ServiceProvider
            Return serviceProvider.GetService(serviceType)
        End Function

#End Region

    End Class

End Namespace

#End Region

And finally the method extensions module for Component class and derivateds like Control, UserControl or Form:

ComponentExtensions.vb

Imports System.ComponentModel.Design
Imports System.Drawing.Design
Imports System.Reflection
Imports System.Windows.Forms.Design

Imports DevCase.Core.Design.Services

#Region " Component Extensions "

Namespace DevCase.Extensions.ComponentExtensions

    ''' <summary>Contains custom extension methods to use with <see cref="System.ComponentModel.Component"/>.</summary>
    <HideModuleName>
    Public Module ComponentExtensions

#Region " Public Methods "

        ''' <summary>Invokes the default <see cref="UITypeEditor"/> to edit the specified property.</summary>
        ''' <param name="component">The source <see cref="System.ComponentModel.Component"/>.</param>
        ''' <param name="[property]">The <see cref="PropertyDescriptor"/> to edit.</param>
        ''' <returns>The resulting value returned by the invoked <see cref="UITypeEditor"/>.</returns>
        <DebuggerStepThrough>
        <Extension>
        <EditorBrowsable(EditorBrowsableState.Always)>
        Public Function InvokeUITypeEditor(Of T)(component As Component, [property] As PropertyDescriptor) As T

            If [property] Is Nothing Then
                Throw New NullReferenceException(NameOf([property]))
            End If

            If [property].PropertyType IsNot GetType(T) Then
                Throw New ArgumentException("Type missmatch.", NameOf(T))
            End If

            Dim result As T
            Dim site As ISite = component.Site

            If site?.DesignMode Then
                Dim designerHost As IDesignerHost = DirectCast(site.GetService(GetType(IDesignerHost)), IDesignerHost)
                Dim designer As IDesigner = designerHost.GetDesigner(component)
                Dim editorServiceContext As Type = (From [type] As Type In GetType(ControlDesigner).Assembly.DefinedTypes Where [type].Name = "EditorServiceContext").Single()
                Dim editValue As MethodInfo = editorServiceContext.GetMethod("EditValue", BindingFlags.Static Or BindingFlags.Public)
                result = DirectCast(editValue.Invoke(Nothing, {designer, component, [property].Name}), T)

            Else
                Dim serviceProvider As IServiceProvider = New ServiceProvider()
                Dim typeDescriptorContext As ITypeDescriptorContext = New TypeDescriptorContext()
                Dim editor As UITypeEditor = DirectCast([property].GetEditor(GetType(UITypeEditor)), UITypeEditor)
                result = DirectCast(editor.EditValue(typeDescriptorContext, serviceProvider, [property].GetValue(component)), T)

            End If

            Return result

        End Function

        ''' <summary>Invokes the default <see cref="UITypeEditor"/> to edit the specified property.</summary>
        ''' <param name="component">The source <see cref="System.ComponentModel.Component"/>.</param>
        ''' <param name="propertyName">The name of the property to edit.</param>
        ''' <returns>The resulting value returned by the invoked <see cref="UITypeEditor"/>. </returns>
        <DebuggerStepThrough>
        <Extension>
        <EditorBrowsable(EditorBrowsableState.Always)>
        Public Function InvokeUITypeEditor(Of T)(component As Component, propertyName As String) As T

            Dim [property] As PropertyDescriptor = TypeDescriptor.GetProperties(component)(propertyName)
            Return InvokeUITypeEditor(Of T)(component, [property])

        End Function

#End Region

    End Module

End Namespace

#End Region

After importing this namespace:

Imports DevCase.Extensions.ComponentExtensions

It is as easy to use as this:

Dim lb As ListBox = Me.ListBox1
Dim propName As String = NameOf(lb.Items)

Dim value As ListBox.ObjectCollection = 
    lb.InvokeUITypeEditor(Of ListBox.ObjectCollection)(propName)

Or this:

Dim lb As ListBox = Me.ListBox1

Dim propDescriptor As PropertyDescriptor = 
    TypeDescriptor.GetProperties(lb)(NameOf(lb.Items)) 

Dim value As ListBox.ObjectCollection = 
    lb.InvokeUITypeEditor(Of ListBox.ObjectCollection)(propDescriptor)
ElektroStudios
  • 19,105
  • 33
  • 200
  • 417