How to display/merge complex objects?

Grids for WPF Forum

Posted 6 years ago by Franco Caloni
Version: 13.1.0581
Avatar

Hello,
I'm testing the PropertyGrid to see if it fit my needs. I have "complex objects" where I means that I must display multiple objects (so using PropertyGrid.SelectedObjects property) which have collections of objects, and so on.

Suppose this is my model (for brevity I have removed the INotifyPropertyChanged implementation):

 


    public class Carico : INotifyPropertyChanged
    {
        private double _v1;
        public double v1 { get { return _v1; } set { _v1 = value; NotifyPropertyChanged("v1"); } }

        private double _v2;
        public double v2 { get { return _v2; } set { _v2 = value; NotifyPropertyChanged("v2"); } }

        private double _v3;
        public double v3 { get { return _v3; } set { _v3 = value; NotifyPropertyChanged("v3"); } }
    }


    public class Asta : INotifyPropertyChanged
    {
        private ObservableCollection<Carico> _Carichi;
        [Category("Carichi")]
        public ObservableCollection<Carico> Carichi { get { return _Carichi; } set { _Carichi = value; NotifyPropertyChanged("Carichi"); } }

        public Asta()
        {
            this.Carichi = new ObservableCollection<Carico>();
            this.Carichi.Add(new Carico() { v1 = 321.0, v2 = 999, v3 = 9812 });
            this.Carichi.Add(new Carico() { v1 = 987.0, v2 = 999, v3 = 1133 });
            this.Carichi.Add(new Carico() { v1 = 159.0, v2 = 999, v3 = 4561 });
        }
      
    }


    public class Contesto : INotifyPropertyChanged
    {
        private ObservableCollection<Asta> _Aste;
        public ObservableCollection<Asta> Aste { get { return _Aste; } set { _Aste = value; NotifyPropertyChanged("Aste"); } }

        public Contesto()
        {
            this.Aste = new ObservableCollection<Asta>();
            this.Aste.Add(new Asta());
            this.Aste.Add(new Asta());
        }

    }

 

as you can see, for semplicity, when a instance of class Contesto is created it add 2 elements in the "Aste" collection, Asta add 3 instances of Class Carico in the Asta.Carichi Collection.

My needs are that when the PropertyGrid show the Contesto.Aste Collection (via the SelectedObjects=Contesto.Aste) it display a category "Carichi" with 3 TextBoxes (one for each v1, v2, v3 properties of Class Carico) where common values for all the Carico instances of all the Asta instances are shown.
And of course when I change the value in one textbox it is setted in all the Carico instances for all the Asta instances.

I'm trying to subclass the TypeDescriptorFactory and overriding the GetProperties(object[] values, DataFactoryOptions options) but i'm not so skilled and don't know how to merge objects/properties.

I'm also creating a CategoryEditor with relative template but I think I need to correctly merge the properties/object...

        <pg:PropertyGrid
            AreAttachedPropertiesBrowsable="False"
            CollectionDisplayMode="EditableInline"
            AreNestedCategoriesSupported="True" 
            PropertyExpandability="ForceSimple"
            IsSummaryVisible="True"
            SummaryCanAutoSize="True"
            SummaryHeight="Auto"
            SelectedObjects="{Binding Aste }"
            Grid.Column="1" Grid.Row="1"
            >

            <pg:PropertyGrid.CategoryEditors  >
                <pg:CategoryEditor Description="Definizione dei Carichi" Category="Carichi"
                            EditorTemplate="{StaticResource ResourceKey=TF}"  >
                    <pg:CategoryEditor.Properties>
                        <pg:CategoryEditorProperty   PropertyName="Carichi"      />
                    </pg:CategoryEditor.Properties>
                </pg:CategoryEditor>
            </pg:PropertyGrid.CategoryEditors>

        </pg:PropertyGrid>


        <DataTemplate x:Key="TF">
            <ItemsControl ItemsSource="{Binding Properties , RelativeSource={RelativeSource AncestorType={x:Type pg:ICategoryEditorDataAccessor}}}">
                <ItemsControl.ItemsPanel>
                    <ItemsPanelTemplate>
                        <StackPanel></StackPanel>
                    </ItemsPanelTemplate>
                </ItemsControl.ItemsPanel>
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <StackPanel Orientation="Vertical" >

                            <Grid x:Name="GrdmainTemplCarichi" DataContext="{Binding Values[0]}">
                                <Grid.RowDefinitions>
                                    <RowDefinition  Height="24*"/>
                                    <RowDefinition  Height="24*"/>
                                    <RowDefinition  Height="240*"/>
                                </Grid.RowDefinitions>
                                <StackPanel Orientation="Vertical" Grid.Row="0">
                                    <TextBox  Text="{Binding [0].v1}"/>
                                    <TextBox  Text="{Binding [0].v2}"/>
                                    <TextBox  Text="{Binding [0].v3}"/>

                                </StackPanel>
                            </Grid>



                            <Grid x:Name="GrdmainTemplCarichi1" DataContext="{Binding Values}">
                                <Grid.RowDefinitions>
                                    <RowDefinition  Height="24*"/>
                                    <RowDefinition  Height="24*"/>
                                    <RowDefinition  Height="240*"/>
                                </Grid.RowDefinitions>
                                <StackPanel Orientation="Vertical" Grid.Row="0">
                                    <TextBox  Text="{Binding [0].v1}"/>
                                    <TextBox  Text="{Binding [0].v2}"/>
                                    <TextBox  Text="{Binding [0].v3}"/>
                                    <TextBox  Text="{Binding v1}"/>
                                    <Label Content="------"/>
                                </StackPanel>
                            </Grid>
                        </StackPanel>

                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>
        </DataTemplate>

any help appreciated, hope this can be done with PropertyGrid

thanks, Franco

Comments (3)

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

Hi Franco,

What you are describing is certainly possible, but it's not something that is supported out of the box. We don't support merging collections, as that can be very difficult to keep in sync. Assuming each of your Asta objects will have the exact same number of Carichi objects, then you could probably build a DataFactory that specifically handles that. Or if you handle cases where one object has more or less items than the other objects, then it should be fine also.

You are correct in that you would need a custom DataFactory, but you would also need one or more custom IPropertyDataAccessor objects. The DataFactory's GetDataAccessors(object[] values, DataFactoryOptions options) is probably the best method to override also.

Your override would need to see if the values parameter is a list of Asta objects. You would then probably want to:

  1. Create a List<IDataAccessor>, which will be returned from the method.
  2. Get the first Carico object from each Asta object - This gets you the objects you will merge
  3. Create an object[] of these objects - This will be used to merge the properties next
  4. Call base.GetProperties(object[] values, DataFactoryOptions options), passing in the object array from #2 - This will return a list of IPropertyDataAccessors. One for each property on Carico, and each one will get/set values on each of the objects from the collection.
  5. Create an instance of CategoryDataAccessor, passing a display name (i.e. "[0]", "First", etc) - This will be used to group the v1/v2/v3 separately from the other entries.
  6. Add the IPropertyDataAccessors from #4 to the CategoryDataAccessor's Accessors collection - This adds the properties to our "group".
  7. Add the CategoryDataAccessor to the list from #1.
  8. Repeat steps 2-8 for all subsequent Carico objects in the Asta objects.
  9. Return the list from #1.

This should provide you with 3 categories, each with v1/v2/v3 properties that get/set values on each of the Carico objects.

Your category editor would need to acces the v1/v2/v3 properties, not the Carichi property. The properties you define, should be the ones you want the category editor to modify. So:

<pg:CategoryEditor.Properties>
    <pg:CategoryEditorProperty   PropertyName="v1"      />
    <pg:CategoryEditorProperty   PropertyName="v2"      />
    <pg:CategoryEditorProperty   PropertyName="v3"      />
</pg:CategoryEditor.Properties>

 Then you'd need to assign it to the appropriate category (i.e. "[0]", "First", etc) by setting the category name. Alternatively, you could add a CategoryEditorDataAccessor via your DataFactory, so it is picked up by all your categories (i.e. "[0]", "[1]", etc). To do this, you would:

  1. Create a List<IDataAccessor>, which will be returned from the method.
  2. Get the first Carico object from each Asta object - This gets you the objects you will merge
  3. Create an object[] of these objects - This will be used to merge the properties next
  4. Call base.GetProperties(object[] values, DataFactoryOptions options), passing in the object array from #2 - This will return a list of IPropertyDataAccessors. One for each property on Carico, and each one will get/set values on each of the objects from the collection.
  5. Create an instance of CategoryDataAccessor, passing a display name (i.e. "[0]", "First", etc) - This will be used to group the v1/v2/v3 separately from the other entries.
  6. Create an instance of CategoryEditorDataAccessor, passing in a display name/description. You would also want to set the EditorTemplateKey property to a known key (i.e. "TF", as you have it above).
  7. Add the IPropertyDataAccessors from #4 to the CategoryEditorDataAccessor's Properties collection - This adds the properties to the editor
  8. Add the CategoryEditorDataAccessor to the CategoryDataAccessor's Accessors collection.
  9. Add the CategoryDataAccessor to the list from #1.
  10. Repeat steps 2-9 for all subsequent Carico objects in the Asta objects.
  11. Return the list from #1.

That should build the categories, each with a custom category editor that can access the 3 properties.


Actipro Software Support

Posted 6 years ago by Franco Caloni
Avatar

Many thanks for your reply, after doing some try I found how to do it, and be able to display in a single category "Carichi" a textbox for every Carico property for all the selected Asta(s).
Here is the code:

Model:

 

    public class Carico : INotifyPropertyChanged
    {
        public int foreignkey { set; get; }

        private double _v1;
        public double v1 { get { return _v1; } set { _v1 = value; NotifyPropertyChanged("v1"); } }

        private double _v2;
        public double v2 { get { return _v2; } set { _v2 = value; NotifyPropertyChanged("v2"); } }

        private double _v3;
        public double v3 { get { return _v3; } set { _v3 = value; NotifyPropertyChanged("v3"); } }

        private double _v4;
        public double v4 { get { return _v4; } set { _v4 = value; NotifyPropertyChanged("v4"); } }

    }


    public class Asta : INotifyPropertyChanged
    {
        [Browsable(false)]
        public int id { set; get; }


        private ObservableCollection<Carico> _Carichi;
        [Browsable(false)]
        public ObservableCollection<Carico> Carichi { get { return _Carichi; } set { _Carichi = value; NotifyPropertyChanged("Carichi"); } }

        private string _testo;
        [Category("Stringhe")]
        public string Testo { get { return _testo; } set { _testo = value; NotifyPropertyChanged("Testo"); } }


        public Asta()
        {
            this.Carichi = new ObservableCollection<Carico>();
            this.Carichi.Add(new Carico() { v1 = 321.0, v2 = 999, v3 = 9812, foreignkey = util.getID() });
            this.Carichi.Add(new Carico() { v1 = 987.0, v2 = 999, v3 = 1133, foreignkey = util.getID() });
            this.Carichi.Add(new Carico() { v1 = 159.0, v2 = 999, v3 = 4561, foreignkey = util.getID() });
        }
    }

    public class Contesto : INotifyPropertyChanged
    {
        private ObservableCollection<Asta> _Aste;
        public ObservableCollection<Asta> Aste { get { return _Aste; } set { _Aste = value; NotifyPropertyChanged("Aste"); } }

        public Contesto()
        {
            this.Aste = new ObservableCollection<Asta>();
            this.Aste.Add(new Asta() { Testo = "prova1", id = util.getID() });
            this.Aste.Add(new Asta() { Testo = "prova", id = util.getID() });
        }
    }

 

the GetDataAccessors override:

 

  public override IList<IDataAccessor> GetDataAccessors(object[] values, DataFactoryOptions options)
        {
            IList<IDataAccessor> ret;
            ret = new List<IDataAccessor>();

            if (values != null)
            {
                var gby = values.GroupBy(z => z.GetType());

                if (gby.Count() == 1)
                {
                    if (gby.ElementAt(0).Key == typeof(Asta))
                    {
                        ret = new List<IDataAccessor>();

                        //  get all the Asta objects in the PropertyGrid.SelectedObjects
                        IEnumerable<Asta> la = values.Cast<Asta>();

                        //  get all the Carichi istances for all the selected Asta in the SelectedObjects property
                        var tuttiCarichi = la.SelectMany(a => a.Carichi).ToArray();
                        CategoryDataAccessor catAccCarichi = new CategoryDataAccessor("Carichi");
                        var ipdaCarichi = base.GetProperties(tuttiCarichi, options);

                        //  add the carichi properties 
                        foreach (var item in ipdaCarichi)
                        {
                            catAccCarichi.Accessors.Add(item);
                        }
                        ret.Add(catAccCarichi);


                        //  add all other properties of Asta
                        var ipAsta = base.GetProperties(values, options);
                        CategoryDataAccessor catAccAsta = new CategoryDataAccessor("Asta");
                        foreach (var item in ipAsta)
                        {
                            catAccAsta.Accessors.Add(item);
                        }
                        ret.Add(catAccAsta);
                    }
                    else
                    {
                        ret = base.GetDataAccessors(values, options);
                    }
                }
                else
                {
                    ret = base.GetDataAccessors(values, options);
                }
            }
            else ret = null;


            return ret;

        }

 

the PropertyGrid have   SelectedObjects="{Binding Contesto.Aste}"
this give me what I need:

Click me

Now I have a little more complicated need, I need to filter which Carico(s) will be added to the "tuttiCarichi" variable so that the ones than meet a criteria will be considered and added as Target for the catAccCarichi.Accessors.

This criteria in stored in the Carico property "foreignkey".

In the Category template I need that the property "foreignkey" is displayed in a combox (let's ignore how the Combo is filled) and when the user change the value in the combo the PropertyGrid should filter which instances of Carico are be added in the tuttiCarichi variable, and so included in the CategoryDataAccessor.Target list.
Just the Carico(s) instances where the value of foreignkey property is equals to the SelectedValue of the ComboBox.

Hope I explained in a clear way

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

Hi Franco,

You have two choices as to where you can enforce the "foreignkey" property (i.e. decide which instances are included/updated). The first is in the DataFactory when you create the data accessors. You can simply look at the "foreignkey" property and decided which instances to pass to the base.GetProperties. In your code above, if it doesn't pass the requirement then you would not add it to your tuttiCarichi collection.

The second choice is to build that logic into a custom MergedPropertyDataAccessor. This object is effectively what is returned by the base.GetProperties for each "merged" property. You can easily inject a custom MergedPropertyDataAccessor by overriding the CreateMergedPropertyDataAccessor method. This method is passed the data accessor for the parent property (if any) and the data accessors for all the properties to be merged. In this case, your custom MergedPropertyDataAccessor would need to decide which data accessors to get/set the value on. Something like:

protected override object ValueInternal {
	get {
		object value = this.PropertyDataAccessors[0].Value;
		if (value != null) {
			for (int i = 1; i < this.PropertyDataAccessors.Count; i++) {
				if (!value.Equals(this.PropertyDataAccessors[i].Value))
					return null;
			}
		}

		return value;
	}
	set {
		foreach (IPropertyDataAccessor propertyDataAccessor in this.PropertyDataAccessors)
			propertyDataAccessor.Value = value;
	}
}

 The disadvantage of the first approach is that you have to refresh the entire PropertyGrid when the foreignkey property is changed. With the custom MergedPropertyDataAccessor, you can simply refresh the merged properties when your ComboBox value is changed.


Actipro Software Support

The latest build of this product (v2018.1 build 0675) was released 8 days ago, which was after the last post in this thread.

Add Comment

Please log in to a validated account to post comments.