Search code examples
c#wpfpropertygridxceed

xceed propertygrid: choose between sets of custom propertydefinitions


Until now, I use the Xceed propertygrid with AutoGenerateProperties="True" and specify which properties I don't want to display by using the [Browsable(false)] attribute.

Now I have a case, where I need to show only a subset of properties, depending on the value of an other property.

I think the answer is to use PropertyDefinitions, as shown here.

But the example only shows how to implement a single set of PropertyDefinitions that will be used for every inspected object.

Do you have a hint or an example of how I can define multiple sets of PropertyDefinitions and choose when they should be used?


Solution

  • Sorry this is quite a lengthy answer. I also used to use AutoGenerateProperties="True" however I had a similar issue.

    My resulting solution was to populate the PropertyDefinition as you suggested. I did this programmatically as I ended up having some rather special cases with wanting properties created dynamically based on the contents of a collection somewhere. One thing I did notice was that even through programmatically checking through my classes to emulate the AutoGenerateProperties="True" feature mine was much faster than their solution.

    In order to deal with the Visibility as you requested though I had to create my own Attribute to make items invisible from the PropertyGrid.

    public class VisibilityAttribute : Attribute
    {
        public Visibility Visibility { get; private set; }
    
        public VisibilityAttribute(Visibility visibility)
        {
            this.Visibility = visibility;
        }
    }
    

    Each item that can be selected in my project has it's own implementation of a PropertySetBase class that has property definitions like so:

    [Category("Appearance"), LocalisedProperty(typeof(PropertySetBase), "LocalBackground", "LocalBackgroundDescription"), Editor(typeof(OptionalBrushEditor), typeof(OptionalBrushEditor)),
            PropertyOrder(0)]
        public virtual OptionalProperty<Brush> LocalBackground { get; set; }
    

    And the logic to then process the PropertySetBases in the currentPropertySelection collection.

    private void PreparePropertyGrid()
    {
        PropertyDefinitionCollection propertyDefinitions = new PropertyDefinitionCollection();
    
        // This is how I determine 
        var mainPropertySet = this.currentPropertySelection.FirstOrDefault();
    
        if (mainPropertySet != null)
        {
            var properties = TypeDescriptor.GetProperties(mainPropertySet.GetType());
            // Allowing for multiple selection, if on further iterations through the selected items we will remove properties that do not exist in both PropertySets
            bool firstIteration = true;
    
            foreach (var x in this.currentPropertySelection)
            {
                foreach (var p in properties.Cast<PropertyDescriptor>())
                {
                    if (!firstIteration)
                    {
                        // Perhaps we should be checking a little more safely for TargetProperties but if the collection is empty we have bigger problems.
                        var definition = propertyDefinitions.FirstOrDefault(d => string.Equals(d.TargetProperties[0] as string, p.Name, StringComparison.Ordinal));
    
                        // Someone in the selection does not have this property so we can ignore it.
                        if (definition == null)
                        {
                            continue;
                        }
    
                        // If this item doesn't have the property remove it from the display collection and proceed.
                        var localProperty = x.GetType().GetProperty(p.Name);
                        if (localProperty == null)
                        {
                            propertyDefinitions.Remove(definition);
                            continue;
                        }
    
                        // There is actually no point in proceeding if this is not the first iteration and we have checked whether the property exists.
                        continue;
                    }
    
                    string category = p.Category;
                    string description = p.Description;
                    string displayName = p.DisplayName ?? p.Name;
                    int? displayOrder = null;
                    bool? isBrowsable = p.IsBrowsable;
                    bool? isExpandable = null;
    
                    var orderAttribute = p.Attributes[typeof(PropertyOrderAttribute)] as PropertyOrderAttribute;
                    if (orderAttribute != null)
                    {
                        displayOrder = orderAttribute.Order;
                    }
    
                    var expandableAttribute = p.Attributes[typeof(ExpandableObjectAttribute)] as ExpandableObjectAttribute;
                    if (expandableAttribute != null)
                    {
                        isExpandable = true;
                    }
    
                    propertyDefinitions.Add(new PropertyDefinition
                    {
                        Category = category,
                        Description = description,
                        DisplayName = displayName,
                        DisplayOrder = displayOrder,
                        IsBrowsable = isBrowsable,
                        IsExpandable = isExpandable,
                        TargetProperties = new[] { p.Name },
                    });
                }
            }
    
            firstIteration = false;
    
            this.propertyGrid.PropertyDefinitions = propertyDefinitions;
        }
    }
    

    When it came to actually showing/hiding the properties I did the following:

    public void UpdateProperties(Tuple<string, bool?, Visibility?>[] newPropertyStates)
    {
        // Note this currently works under the assumption that an Item has to be selected in order to have a value changed.
        this.suppressPropertyUpdates = true;
    
        foreach (var property in newPropertyStates)
        {
            string propertyName = property.Item1;
    
            string[] splits = propertyName.Split('.');
            if (splits.Length == 1)
            {
                this.propertyGrid.Properties.OfType<PropertyItem>()
                                            .Where(p => string.Equals(p.PropertyDescriptor.Name, propertyName, StringComparison.Ordinal))
                                            .Map(p =>
                {
                    if (property.Item2.HasValue)
                    {
                        p.IsEnabled = property.Item2.Value;
                    }
    
                    if (property.Item3.HasValue)
                    {
                        p.Visibility = property.Item3.Value;
                    }
                });
    
            }
            else // We currently don't expect to go any lower than 1 level.
            {
                var parent = this.propertyGrid.Properties.OfType<PropertyItem>()
                                                         .Where(p => string.Equals(p.PropertyDescriptor.Name, splits[0], StringComparison.Ordinal))
                                                         .FirstOrDefault();
    
                if (parent != null)
                {
                    parent.Properties.OfType<PropertyItem>()
                                     .Where(p => string.Equals(p.PropertyDescriptor.Name, splits[1], StringComparison.Ordinal))
                                     .Map(p =>
                    {
                        if (property.Item2.HasValue)
                        {
                            p.IsEnabled = property.Item2.Value;
                        }
                        if (property.Item3.HasValue)
                        {
                            p.Visibility = property.Item3.Value;
                        }
                    });
                }
            }
        }
    
        this.suppressPropertyUpdates = false;
    }
    

    Then inside the PreparePropertyItem event handler I check for my VisibilityAttribute and update the property item accordingly.

    void PropertyGrid_PreparePropertyItem(object sender, PropertyItemEventArgs e)
    {
        foreach (var x in this.currentPropertySelection)
        {
            // If we are in read-only mode do not allow the editing of any property.
            if (this.IsReadOnly)
            {
                e.PropertyItem.IsEnabled = false;
            }
    
            string propertyName = ((PropertyItem)e.PropertyItem).PropertyDescriptor.Name;
            PropertyInfo property = x.GetType().GetProperty(propertyName);
            var propertyItem = e.Item as PropertyItem;
    
            // If the property doesn't exist then check to see if it is on an expandable item.
            if (property == null)
            {
                property = propertyItem.Instance.GetType().GetProperty(propertyName);
            }
    
            bool hasProperty = property != null;
    
            if (hasProperty)
            {
                var browsableAttribute = property.GetCustomAttribute<BrowsableAttribute>(true);
                if (browsableAttribute != null &&
                    !browsableAttribute.Browsable)
                {
                    e.PropertyItem.Visibility = Visibility.Collapsed;
                    e.Handled = true;
                    break;
                }
    
                var visibilityAttribute = property.GetCustomAttribute<VisibilityAttribute>(true);
                if (visibilityAttribute != null)
                {
                    e.PropertyItem.Visibility = visibilityAttribute.Visibility;
                    e.Handled = true;
                }
    
                var independentAttribute = property.GetCustomAttribute<IndependentAttribute>(true);
                // If a property is marked as being independent then we do not allow editing if multiple items are selected
                if (independentAttribute != null &&
                    this.currentPropertySelection.Length > 1)
                {
                    e.PropertyItem.IsEnabled = false;
                    e.Handled = true;
                    break;
                }
            }
        }
    }