Custom editor for PropertyGrid in it's simplest form

Editors for WPF Forum

Posted 6 years ago by Cory Foley
Version: 13.1.0580
Avatar

Okay, I think I am just getting a bit lost here with all the pieces here and can't seem to follow them. New to both WPF and Actipro and this could be a result of a lack of understanding of both honestly. 

I am trying to create a custom PropertyEditor but keep getting lost when looking through the sample code because it's broken up into so many pieces and the samples seems a bit overly complex in some ways. 

Basically, what I am trying to do is create a property editor that I can use with Lazy Loading for a class that looks like this

public class DynamicEnum
{
    public string SelectedItem {get;set;}
    public List<string> Items {get;set;}
}

What I would like is to create a ComboBox out of it that pretty much works like an BuiltinEditors.ComboBoxValueTemplateKey. The idea would be I could propulate a list that needs to be created at run-time and use it like a Enum where I can select from a ComboBox an item from that list. 

But I keep getting lost, I don't know what pieces I need to be making for this and the examples keep running me in circles. I don't know if I need to be making a DataFactory, PropertyEditor, TypeConverter, EditBoxCachedPropertyDataAccessorBase, or whatever else. I just keep getting lost trying to find a complete, simple example of creating a Custom Property Editor that I can easily use as a Lazy Loaded editor like this:

[Editor(typeof(DynamicEnumPropertyEditor), typeof(PropertyEditor))]
public DynamicEnum SponsorList {get;set;}

If someone could help me out with a very simple example, it would be much appriciated. 

Thanks in advance.

[Modified 6 years ago]

Comments (5)

Posted 6 years ago by Cory Foley
Avatar

Okay, well here's what I have working so far, but it's quite obvious that this is a hack at the very least. 

Template:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
		xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
		xmlns:editors="http://schemas.actiprosoftware.com/winfx/xaml/editors"
		xmlns:sample="clr-namespace:MyProjectsNamespace"
		xmlns:propgrid="http://schemas.actiprosoftware.com/winfx/xaml/propgrid">
    <DataTemplate x:Key="{x:Static sample:PropertyEditors.DynamicEnumTemplateKey}">
        <ComboBox x:Name="editBox" Margin="0" Padding="0" HorizontalAlignment="Stretch" 
            VerticalAlignment="Stretch" BorderThickness="0" Background="Transparent" 
            SelectedItem="{Binding Value, RelativeSource={RelativeSource 
                AncestorType={x:Type propgrid:IPropertyDataAccessor}}, Mode=OneWayToSource, 
                ValidatesOnExceptions=True, ValidatesOnDataErrors=True, NotifyOnValidationError=True, 
                UpdateSourceTrigger=PropertyChanged}"
	    ItemsSource="{Binding Value, RelativeSource={RelativeSource 
                AncestorType={x:Type propgrid:IPropertyDataAccessor}}, ValidatesOnExceptions=True, 
                ValidatesOnDataErrors=True, NotifyOnValidationError=True, 
                UpdateSourceTrigger=PropertyChanged}" 
         />
    </DataTemplate>

</ResourceDictionary>

PropertyEditor Code:

namespace MyProjectsNamespace
{
    public static class PropertyEditors
    {

        // Value Template Resource Keys
        private static ComponentResourceKey dynamicEnumTemplateKey;

        public static ResourceKey DynamicEnumTemplateKey
        {
            get
            {
                if (dynamicEnumTemplateKey == null)
                    dynamicEnumTemplateKey = 
                       new ComponentResourceKey(typeof(PropertyEditors), "DynamicEnumTemplate");
                return dynamicEnumTemplateKey;
            }
        }
    }

    public class DynamicEnumPropertyEditor : PropertyEditor
    {

        public override DataTemplate ValueTemplate
        {
            get { return null; }
            set { /* No-op */ }
        }

        public override object ValueTemplateKey
        {
            get { return PropertyEditors.DynamicEnumTemplateKey; }
        }

        public override DataTemplateSelector ValueTemplateSelector
        {
            get { return null; }
            set { Console.WriteLine("TEST: " + value); /* No-op */ }
        }

    }
}

In Use:

public class TestObject
{ 
    private string selectedSponsor = "None";
    private string[] sponsorList = new string[] { "None", "Sponsor1", "Sponsor2", "Sponsor3" };
    public string GetSponsor()
    {
        return selectedSponsor;
    }

    [Category("Sponsor")]
    [Description("Select the sponsor for this item.")]
    [Editor(typeof(DynamicEnumPropertyEditor), typeof(PropertyEditor))]
    [TypeConverter(typeof(StringConverter))]
    public object Sponsor 
    {
         //The get provides the whole list to the object which is fed into the combobox
         get { return sponsorList; }
         //The set is one way (to source) bound to the object and gets the selected item.
         set { selectedSponsor = (value ?? "").ToString(); }
     
         //The whole thing is an object because it gets and sets different types.
         //The TypeConverter is set to StringConverter to prevent the array from having expanded properties.
    }
} 

 
As you can see, it's a total hack as of now.

What does work:
► It does display correctly
► It does not have expanded items underneath it for each array value because of the TypeConverter being set to StringConverter
► It does return the proper value to the Setter of the source object.

What does not work:
► With this method, I cannot dynamically set the source object in code.
► This method doesn't allow me to create a single object that does all of this, instead I am using multiple variables to get the proper outcome
 

I suspect that I need to create a custom IPropertyDataAccessor to do this properly from a single object like I displayed in the original post. But this is where I am currently at. Any pointers to move forward will be appriciated. 


[Modified 6 years ago]

Posted 6 years ago by Cory Foley
Avatar

Okay, well now I have something that's fully working, but I still think I may have been doing something wrong here. But it works, so I won't complain for now. But any additional advice would be appriciated. 

Template:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
		xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
		xmlns:sample="clr-namespace:MyProjectsNamespace"
		xmlns:propgrid="http://schemas.actiprosoftware.com/winfx/xaml/propgrid">
    <DataTemplate x:Key="{x:Static sample:PropertyEditors.DynamicEnumTemplateKey}">
        <sample:DynamicEnumComboBox x:Name="editBox" Margin="0" Padding="0" 
                HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
		BorderThickness="0" Background="Transparent"
                DynamicEnumObject="{Binding Value, RelativeSource={RelativeSource 
                    AncestorType={x:Type propgrid:IPropertyDataAccessor}}}" />
    </DataTemplate>
</ResourceDictionary>

Back-end Code: 

namespace MyProjectsNamespace
{
    public static class PropertyEditors
    {
        // Value Template Resource Keys
        private static ComponentResourceKey dynamicEnumTemplateKey;

        public static ResourceKey DynamicEnumTemplateKey
        {
            get
            {
                if (dynamicEnumTemplateKey == null)
                    dynamicEnumTemplateKey = 
                        new ComponentResourceKey(typeof(PropertyEditors), "DynamicEnumTemplate");
                return dynamicEnumTemplateKey;
            }
        }
    }

    /// <summary>
    /// Object base used by the DynamicEnumPropertyEditor
    /// </summary>
    public class DynamicEnum : INotifyPropertyChanged
    {
        private string selectedItem = "None";
        public string SelectedItem
        {
            get { return selectedItem; }
            set
            {
                selectedItem = value;
                OnPropertyChanged("SelectedItem");
            }
        }
        private string[] items = new string[] { "None" };
        public string[] Items
        {
            get { return items; }
            set
            {
                items = value;
                OnPropertyChanged("Items");
            }

        }
        public override string ToString()
        {
            return SelectedItem;
        }

        public event PropertyChangedEventHandler PropertyChanged;

        private void OnPropertyChanged(string info)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(info));
            }
        }
    }

    /// <summary>
    /// Property editor used to choice a single item from an array of items.
    /// </summary>
    public class DynamicEnumPropertyEditor : PropertyEditor
    {
        public override DataTemplate ValueTemplate
        {
            get { return null; }
            set { /* No-op */ }
        }

        public override object ValueTemplateKey
        {
            get { return PropertyEditors.DynamicEnumTemplateKey; }
        }

        public override DataTemplateSelector ValueTemplateSelector
        {
            get { return null; }
            set { /* No-op */ }
        }
    }
    /// <summary>
    /// Custom control used to bind the DynamicEnum object to it's various parts. 
    /// </summary>
    public class DynamicEnumComboBox : ComboBox
    {
        public DynamicEnumComboBox()
        {
            this.SetResourceReference(StyleProperty, typeof(ComboBox));
        }

        private DynamicEnum bindingObject = null;
        private Binding selectedItemBinding = null;
        public DynamicEnum DynamicEnumObject
        {
            get
            {
                return (DynamicEnum)GetValue(DynamicEnumObjectProperty);
            }
            set
            {
                SetValue(DynamicEnumObjectProperty, value);
                bindingObject = (DynamicEnum)value;
                if (bindingObject != null)
                {
                    ItemsSource = bindingObject.Items;
                    if (selectedItemBinding == null)
                    {
                        selectedItemBinding = new Binding("SelectedItem");
                        selectedItemBinding.Source = bindingObject;
                        selectedItemBinding.Path = new PropertyPath("SelectedItem");
                        selectedItemBinding.Mode = BindingMode.TwoWay;
                        this.SetBinding(SelectedItemProperty, selectedItemBinding);
                    }
                }
            }
        }

        public static readonly DependencyProperty DynamicEnumObjectProperty =
            DependencyProperty.Register("DynamicEnumObject", typeof(object), 
                typeof(DynamicEnumComboBox), new PropertyMetadata(OnDynamicEnumPropertyChanged));

        static void OnDynamicEnumPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var item = d as DynamicEnumComboBox;
            if(item != null)
                item.DynamicEnumObject = (DynamicEnum)item.GetValue(DynamicEnumObjectProperty);
        }
    }
}

In Use:

// To be lazy loaded by PropertyGrid
public class TestObject
{
    //Must be initialized or it won't bind.
    private DynamicEnum sponsor = new DynamicEnum();

    [Category("Sponsor")]
    [Description("Select the sponsor for this item.")]
    [Editor(typeof(DynamicEnumPropertyEditor), typeof(PropertyEditor))]
    public DynamicEnum Sponsor
    {
        get { return sponsor; }
        set { sponsor = value; }
    }
}

I still am not positive on how I should be doing it, so I would still like to hear some feedback if possible. 

[Modified 6 years ago]

Posted 6 years ago by Actipro Software Support - Cleveland, OH, USA
Avatar

Hi Cory,

I'm sorry, but I'm a bit lost about what you are trying to accomplish. Let's back up a bit. Can you please explain how you would like the "Sponsor" property on your TestObject to look? Here's how I see it based solely on your object's signature, which I'm assuming is:

public class DynamicEnum
{
    public string SelectedItem {get;set;}
    public List<string> Items {get;set;}
}

If you have an object like so:

public class TestObject
{
    private DynamicEnum sponsor = new DynamicEnum();
 
    public DynamicEnum Sponsor
    {
        get { return sponsor; }
        set { sponsor = value; }
    }
}

 Then by default, there will be 1 property shown by the PropertyGrid, called Sponsor. You will not be able to expand this property, as the PropertyGrid does not know that is should expand (or really how). This is typically the job of a TypeConverter, which is built into .Net. But our PropertyGrid has another abstraction layer that you could use if you so choose. To make the Sponsor property expandable, you'd need to change your class declaration to:

[TypeConverter(typeof(ExpandableObjectConverter))]
public class DynamicEnum
{
	public string SelectedItem { get; set; }
	public List<string> Items { get; set; }
}

This will allow you to expand the Sponsor property. At this point you now have 3 properties, each of which can have it's own property editor.

There are several ways that you can set property editors or allow your end users to "set" this property. So what are you ultimately trying to accomplish? Do you have a UI mockup that we could take a look at?


Actipro Software Support

Posted 6 years ago by Cory Foley
Avatar

Essentially, I was trying to accomplish this.
I did succeed in what I was trying to do, but don't think I did it correctly. 

And sorry to confuse you, I did NOT want the items in the array to expand, and with my first reply, the method I had in place used a string[] to be loaded in and it ended up creating expanded properties for each element in the array. But now that's worked out. 

I wanted my DynamicEnum to basically act like a normal Enum would in a dropdown. It would list all it's items and you would select an item from that list.

This control allows me to have a control to select an element from a List<string> and then do something with it. Because I am doing lazy loading, I needed this to work automatically, so I had to create my own PropertyEditor. 

The code I have posted above though does work, so you can see what it does by lazy loading the TestObject into a PropertyGrid and going from there.

 

[Modified 6 years ago]

Answer - Posted 6 years ago by Actipro Software Support - Cleveland, OH, USA
Avatar

Hi Cory,

I think I understand now. What you have above isn't really a good way to do this. The list of possible DynamicEnum values should not come from an instance of DynamicEnum. Basically everytime you change your selection, you will rebuild the ComboBox and recreate the same list of items. One thing you could do is make the Items a static property and then just reference it directly (i.e. no binding needed).

In PropertyGrid terms (both WinForms and ours), what you have is typically considered a "standard value". This term comes from the TypeConverter class, which allows you to specific the possible values via a method override called GetStandardValues (http://msdn.microsoft.com/en-us/library/157y67c0.aspx). This is how the .NET enumerations list their values.

You can find more information about this at the following link. But once you implement a custom TypeConverter, then you can do away with your custom property editor, as our default property editor knows how to hook into the associated TypeConverter.

http://msdn.microsoft.com/en-us/library/ayybcxe5(v=vs.90).aspx

Hope this helps.


Actipro Software Support

The latest build of this product (v2019.1 build 0681) was released 9 days ago, which was after the last post in this thread.

Add Comment

Please log in to a validated account to post comments.