This project is read-only.

Restricting collection properties to that of IEnumerable

Apr 9, 2012 at 7:56 AM

Hi Michael and other experts,

We are trying to use this in a semiconductor manufacturing related project, where there are several derived attributes - each manuacturing step's attributes depends on output from previous step. It works well for most of the application (Business layer exposing attributes to WPF UI) , except when exposing custom lists derived from DependentList. We have been trying to understand and workaround this for a while, but have not been able to make much progress. So would like to seek your advice.

There appears to be a constraint while using UpdateControls, that restricts properties exposed for a collection class to those supported by IEnumerable only. I need to expose a collection class (derived from DependentList) that has additional properties (beyond the IEnumerable ones). While digging further into the source code I noticed one place where this restriction appears to be implemented (hard coded):

File: UpdateControls.XAML.Wrapper.ClassProperty, Class: ClassProperty, Function: ClassProperty(PropertyInfo property, Type objectInstanceType), Line #76: valueType = typeof(IEnumerable);

Question: What is the reason for this hardcoding to base class/ assignable type instead of actual type?

 

The other place there is a mention of this restriction is 

File: UpdateControls.XAML.Wrapper.ClassInstance, Class: ClassInstance, Function: ClassInstance(Type wrappedType, Type objectInstanceType), Line #31-#35

************************************SOURCE CODE START**************************************************

// Create a wrapper for each non-collection property.

_classProperties = _wrappedType

    .GetProperties()

    .Select(p => new ClassProperty(p, objectInstanceType))

    .ToList();

 

************************************SOURCE CODE END ***************************************************

Based on my knowledge and reading up, the above code should create a wrapper for each property - both collection and non-collection properties.

Question: Is my understanding incorrect or am I missing something?

Thanks,

Parmesh

Apr 9, 2012 at 2:49 PM

The wrapper should work with any property that is assignable to IEnumerable. You can expose your property as IList, ICollection, List, etc. You should be able to use either the generic or the non-generic forms, as they all implement IEnumerable. You should even be able to use derivatives of DependentList, as it implements IEnumerable.

The valueType property that I hard code to IEnumerable is only used to set up the DependencyProperty for WPF. It should have no impact on the methods available to your code.

Have you defined an interface for your extended DependentList? Are you using this interface as the return type? If so, please be sure that your interface inherits IEnumerable, so that the property is recognized as assignable to IEnumerable.

And finally, I want to be sure that the extensions that you added to your DependentList derivative are not mutators. DependentList was intended to be read-only, since it gets its value from the function that you construct it with. Instead of mutating the list, you should consider mutating its source and allowing the calculation to update it.

Thanks for the question. If my analysis is incorrect, please post some code so that I can help.

Apr 10, 2012 at 12:16 AM

Hi Michael,

I am returning the class derived from DependentList. The business app is a little too big and involved, so I will develop a sample app with this behaviour and post the code - or in the process if I do not see the problem in the sample app, then we know the problem is somewhere else.

Thanks,

Parmesh

Apr 10, 2012 at 11:26 PM

Hi Michael,

I am able to demonstrate the issue with minor changes to the UpdateControls.XAML.Test project (Desktop). I have created a DependentListEx<T> class derived from DependentList<T>, with one extra property CountEx (same as Count in DependentList). I have modified the Window1.xaml file to display the Count and CountEx properties. The value for Count shows up correctly whereas the value for CountEx does NOT show up at all.

Below are the changes to demonstrate the issue, please review and provide your feedback.

Thanks,

Parmesh

ContactListViewModel.cs (Changes are surrounded by //PARMESH)

namespace UpdateControls.XAML.Test
{
    public class ContactListViewModel
	{
		private ContactList _contactList;
		private ContactListNavigationModel _navigation;

        //PARMESH-BEGIN
        private DependentListEx<PersonViewModel> _people;
        //PARMESH-END

		public ContactListViewModel(ContactList contactList, ContactListNavigationModel navigation)
		{
			_contactList = contactList;
			_navigation = navigation;

            //PARMESH-BEGIN
            _people = new DependentListEx<PersonViewModel>(() => _contactList.People.Select(p => PersonViewModel.Wrap(p, _contactList)));
            //PARMESH-BEGIN
		}

        public DataGridContactListViewModel DataGridVM
        {
            get { return new DataGridContactListViewModel(_contactList); }
        }

        //PARMESH-BEGIN
		//public IEnumerable<PersonViewModel> People
        public DependentListEx<PersonViewModel> People
		{
			get 
            {
                //return _contactList.People.Select(p => PersonViewModel.Wrap(p, _contactList));
                return _people;
            }
		}
        //PARMESH-END

		public PersonViewModel SelectedPerson
		{
			get { return PersonViewModel.Wrap(_navigation.SelectedPerson, _contactList); }
			set { _navigation.SelectedPerson = PersonViewModel.Unwrap(value); }
		}

		public bool IsPersonSelected
		{
			get { return _navigation.SelectedPerson != null; }
		}

		public ICommand NewPerson
		{
			get
			{
				return MakeCommand
					.Do(() => _navigation.SelectedPerson = _contactList.NewPerson());
			}
		}

		public ICommand DeletePerson
		{
			get
			{
				return MakeCommand
					.When(() => _navigation.SelectedPerson != null)
					.Do(() => _contactList.DeletePerson(_navigation.SelectedPerson));
			}
		}
	}

    //PARMESH-BEGIN
    public class DependentListEx<T> : DependentList<T>
    {
        public DependentListEx(Func<IEnumerable<T>> computeCollection)
            : base(computeCollection)
        { 
        }
        public int CountEx
        {
            get
            {
                return Count;
            }
        }
    }
    //PARMESH-END
}

 

 

 

Window1.xaml (Changes are in GREEN)

<Window x:Class="UpdateControls.XAML.Test.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="316" Width="559">

    <Window.Resources>
        <Style TargetType="Border">
            <Setter Property="Margin" Value="3"/>
        </Style>
    </Window.Resources>

    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition
                Width="250" />
            <ColumnDefinition
                Width="*" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
        </Grid.RowDefinitions>

        <Border
            Margin="3">
            <ListBox ItemsSource="{Binding People}" SelectedItem="{Binding SelectedPerson}">
                <ListBox.ItemTemplate>
                    <DataTemplate>
                        <TextBlock Text="{Binding FullName}"/>
                    </DataTemplate>
                </ListBox.ItemTemplate>
            </ListBox>
        </Border>

        <StackPanel Grid.Row="1" IsEnabled="{Binding IsPersonSelected}">
            <Grid DataContext="{Binding SelectedPerson}">
                <Grid.RowDefinitions>
                    <RowDefinition Height="auto"/>
                    <RowDefinition Height="auto"/>
                    <RowDefinition Height="auto"/>
                    <RowDefinition Height="auto"/>
                    <RowDefinition Height="auto"/>
                    <RowDefinition Height="auto"/>
                </Grid.RowDefinitions>

                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="auto"/>
                    <ColumnDefinition Width="*"/>
                </Grid.ColumnDefinitions>

                <Border Grid.Row="0" Grid.Column="0">
                    <Label Content="Prefix:"/>
                </Border>
                <Border Grid.Row="0" Grid.Column="1">
                    <ComboBox ItemsSource="{Binding Prefixes}" SelectedItem="{Binding Prefix}"/>
                </Border>
                <Border Grid.Row="1" Grid.Column="0">
                    <Label Content="First Name:"/>
                </Border>
                <Border Grid.Row="1" Grid.Column="1">
                    <TextBox Text="{Binding First, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}"/>
                </Border>
                <Border Grid.Row="2" Grid.Column="0">
                    <Label Content="Last Name:"/>
                </Border>
                <Border Grid.Row="2" Grid.Column="1">
                    <TextBox Text="{Binding Last, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}"/>
                </Border>
                <Border Grid.Row="3" Grid.Column="0">
                    <Label Content="Gender:"/>
                </Border>
                <Border Grid.Row="3" Grid.Column="1">
                    <ComboBox Text="{Binding Gender}">
                        <ComboBoxItem>Male</ComboBoxItem>
                        <ComboBoxItem>Female</ComboBoxItem>
                    </ComboBox>
                </Border>
                <Border Grid.Row="4" Grid.Column="0">
                    <Label Content="Spouse:"/>
                </Border>
                <Border Grid.Row="4" Grid.Column="1">
                    <ComboBox ItemsSource="{Binding PotentialSpouses}" SelectedItem="{Binding Spouse}">
                        <ComboBox.ItemTemplate>
                            <DataTemplate>
                                <TextBlock Text="{Binding FullName}"/>
                            </DataTemplate>
                        </ComboBox.ItemTemplate>
                    </ComboBox>
                </Border>
                <Border Grid.Row="5" Grid.Column="0">
                    <Label Content="NewCount:"/>
                </Border>
                <Border Grid.Row="5" Grid.Column="1">
                    <TextBox Text="{Binding NewCount, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}"/>
                </Border>
            </Grid>
        </StackPanel>

        <StackPanel Grid.Row="2" Orientation="Horizontal">
            <Border>
                <Button Command="{Binding NewPerson}">New Person</Button>
            </Border>
            <Border>
                <Button Command="{Binding DeletePerson}">Delete Person</Button>
            </Border>
            <Border>
                <Button Name="NewWindow" Click="NewWindow_Click">New Window</Button>
            </Border>
        </StackPanel>
        <DataGrid Grid.Column="1" Grid.RowSpan="2"
            Margin="3"
            DataContext="{Binding DataGridVM}"
            ItemsSource="{Binding People}"
            AutoGenerateColumns="False">
            <DataGrid.Columns>
                <DataGridComboBoxColumn
                    Header="Prefix"
                    ItemsSource="{Binding Prefixes}"
                    SelectedItemBinding="{Binding Prefix}" />
                <DataGridTextColumn
                    Binding="{Binding First}"
                    Header="First Name" />
                <DataGridTextColumn
                    Binding="{Binding Last}"
                    Header="Last Name" />
                <DataGridComboBoxColumn
                    Header="Gender"
                    SelectedItemBinding="{Binding Gender}" />
            </DataGrid.Columns>
        </DataGrid>
        <StackPanel Grid.Row="2" Grid.Column="1" Orientation="Horizontal">
            <Grid DataContext="{Binding People}">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="auto"/>
                    <ColumnDefinition Width="auto"/>
                    <ColumnDefinition Width="auto"/>
                    <ColumnDefinition Width="auto"/>
                </Grid.ColumnDefinitions>
                <Border Grid.Column="0">
                    <Label Content="Count:"/>
                </Border>
                <Border Grid.Column="1">
                        <TextBox Text="{Binding Count, Mode=OneWay}"/>
                </Border>
                <Border Grid.Column="2">
                    <Label Content="CountEx:"/>
                </Border>
                <Border Grid.Column="3">
                    <TextBox Text="{Binding CountEx, Mode=OneWay}"/>
                </Border>
            </Grid> 
        </StackPanel>
    </Grid>
</Window>


 

 

Apr 11, 2012 at 4:32 AM

Thank you for the repro. That makes it so much easier. I never envisioned using DependentList in that way. Let me work on that and see what I come up with.

Thanks.

Apr 19, 2012 at 3:14 PM

I can't find a clean way to bind custom properties on a collection type. Instead, I recommend that you use composition (has-a) instead of inheritance (is-a).

The binding code expects a property to either be atomic or a collection. If it is atomic, then it can have properties. If it is a collection, then it can have children. Your DependentListEx tries to do both at the same time.

I recommend that you define your class not as a derived type of DependentList, but as having a DependentList field. For example:

    public class DependentListEx<T>
    {
        private DependentList<T> _children;

        public DependentListEx(Func<IEnumerable<T>> computeCollection)
        {
            _children = new DependentList<T>(computeCollection);
        }

        public IEnumerable<T> Children
        {
            get { return _children; }
        }

        public int CountEx
        {
            get { return _children.Count; }
        }
    }

So your class has a collection, rather than is a collection. This works better with the assumptions in the Update Controls binding code.

Let me know if this works for you.

Apr 24, 2012 at 12:43 AM

I am leveraging UpdateControls with several hieracheis and compositions and so would not be appropriate to go for composition only approach.

I have developed a class class ObservableCollectionWrapped<TWrappedObjectType> : ObservableCollection<object>, ICustomTypeDescriptor

to derive from ObservableCollection<T> that is helping with this behaviour. I am currently testing it and it seems promising. The concepts of TypeDescriptors is new to me, so after completion of testing, I will spend some effort to simplify and post my solution.



Apr 24, 2012 at 2:24 AM

I'm not sure that you need to implement ICustomTypeProvider. If your class implements INotifyCollectionChanged (which it does, since it inherits ObservableCollection), then ForView.Wrap will get out of your way. You'll be able to implement INotifyPropertyChanged yourself. This won't give you dependency tracking, but it's a way for you to use UC for some things and your own mechanism for others. Will this do what you need it to?

Aug 30, 2012 at 10:05 AM

Accept my apologies for nto responding. I overlooked your response and the team used a workaround of wrapping individual elements to get the project going.

I will spend some time on this and post my findings.