fharreau fharreau - 22 days ago 6
C# Question

WPF custom composite user controls

First of all, I am a WPF beginner! My approach is potentially not the right way to do what I want so do not hesitate to tell me if that is the case. What I want to do is a composite user control in WPF, using MVVM.

Some classes will do a better presentation than I, here are my view models:

interface IParameter : INotifyPropertyChanged
{
string Name { get; set;}
string Value { get; set;}
}

class TextParameter : ViewModelBase, IParameter
{
private string _value;

public string Name { get; private set; }

public string Value
{
get { return _value; }
set
{
_value = value;
RaisePropertyChanged();
}
}

public TextParameter (string name)
{
this.Name = name;
}
}

class ParameterList : ViewModelBase, IParameter
{
private string _value;

public string Name { get; private set; }

public string Value
{
get { return _value; }
set
{
_value = value;
RaisePropertyChanged();
}
}

ObservableCollection<IParameter> Parameters { get; set; }

public ParameterList (string name, IEnumerable<IParameter> parameters = null)
{
this.Name = name;
this.Parameters = new ObservableCollection<IParameter>(parameters ?? new List<IParameter>());
}
}


I am using MVVM Light, so all the PropertyChanged stuff is managed into ViewModelBase. Also, this is not an exhaustive list of all the parameters, there is some others, more complex but the issue is about these ones.

Here are my custom user controls:

TextParameterControl.xaml:

<UserControl x:Class="Stuff.TextParameterControl" [..] x:Name="parent">
<StackPanel DataContext="{Binding ElementName=parent}" Orientation="Horizontal">
<TextBlock Text="{Binding Path=ParamName, StringFormat='{}{0}:'}" Width="100"></TextBlock>
<TextBox Text="{Binding Path=Value}" Width="100"></TextBox>
</StackPanel>
</UserControl>


TextParameterControl.xaml.cs :

public class TextParameterControl : UserControl
{
#region param name

public string ParamName
{
get { return (string)GetValue(ParamNameProperty); }
set { SetValue(ParamNameProperty, value); }
}

// Using a DependencyProperty as the backing store for ParamName. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ParamNameProperty =
DependencyProperty.Register("ParamName", typeof(string), typeof(TextParameterControl), new PropertyMetadata(String.Empty));

#endregion

#region value

public string Value
{
get { return (string)GetValue(ValueProperty); }
set { SetValue(ValueProperty, value); }
}

// Using a DependencyProperty as the backing store for Value. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register("Value", typeof(string), typeof(TextParameterControl), new PropertyMetadata(String.Empty));

#endregion

public TextParameterControl()
{
InitializeComponent();
}
}


ParameterListControl.xaml:

<UserControl x:Class="Stuff.ParameterListControl" [..] x:Name="parent">
<UserControl.Resources>
<DataTemplate x:Key="TextParameterTemplate">
<c:TextParameterControl ParamName="{Binding Name}" Value="{Binding Value}"/>
</DataTemplate>
<DataTemplate x:Key="ParameterListTemplate">
<c:ParameterListControl ParamName="{Binding Name}" Value="{Binding Value}" Items="{Binding Parameters}" />
</DataTemplate>
<s:ParameterTemplateSelector x:Key="ParameterSelector"
TextParameterTemplate="{StaticResource TextParameterTemplate}"
ParameterListTemplate="{StaticResource ParameterListTemplate}"/>
</UserControl.Resources>
<Expander DataContext="{Binding ElementName=parent}" Header="{Binding Path=ParamName}" IsExpanded="True" ExpandDirection="Down">
<StackPanel>
<ItemsControl ItemsSource="{Binding Path=Items}" ItemTemplateSelector="{StaticResource ParameterSelector}"></ItemsControl>
</StackPanel>
</Expander>
</UserControl>


ParameterListControl.xaml.cs:

public partial class ParameterListControl: UserControl
{
#region param name

public string ParamName
{
get { return (string)GetValue(ParamNameProperty); }
set { SetValue(ParamNameProperty, value); }
}

// Using a DependencyProperty as the backing store for ParamName. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ParamNameProperty =
DependencyProperty.Register("ParamName", typeof(string), typeof(ParameterListControl), new PropertyMetadata(String.Empty));

#endregion

#region value

public string Value
{
get { return (string)GetValue(ValueProperty); }
set { SetValue(ValueProperty, value); }
}

// Using a DependencyProperty as the backing store for Value. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register("Value", typeof(string), typeof(ParameterListControl), new PropertyMetadata(String.Empty));

#endregion

#region items

public IList<string> Items
{
get { return (List<string>)GetValue(ItemsProperty); }
set { SetValue(ItemsProperty, value); }
}

public static readonly DependencyProperty ItemsProperty =
DependencyProperty.Register("Items", typeof(IList<string>), typeof(ParameterListControl), new PropertyMetadata(new List<string>()));

#endregion

public ParameterListControl()
{
InitializeComponent();
}
}


Here is my custom template selector:

class ParameterTemplateSelector : DataTemplateSelector
{
public DataTemplate ParameterListTemplate { get; set; }
public DataTemplate TextParameterTemplate { get; set; }

public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
if (item is TextParameter)
{
return this.TextParameterTemplate;
}
else if (item is ParameterList)
{
return this.ParameterListTemplate;
}

throw new Exception(String.Format("This parameter ({0}) is not handled in the application", item.GetType().Name));
}
}


And here is the calling View and ViewModel:

ViewModel:

public class MainViewModel : ViewModelBase
{
public ObservableCollection<IParameter> Parameters { get; set; }

public MainViewModel()
{
this.Parameters = new ObservableCollection<IParameter>();
this.Parameters.Add(new TextParameter("Customer"));
// here I am building my complex composite parameter list
}


View:

<UserControl.Resources>
<DataTemplate x:Key="TextParameterTemplate">
<c:TextParameterControl ParamName="{Binding Name}" Value="{Binding Value}"/>
</DataTemplate>
<DataTemplate x:Key="ParameterListTemplate">
<c:ParameterListControl ParamName="{Binding Name}" Value="{Binding Value}" Items="{Binding Parameters}" />
</DataTemplate>

<s:ParameterTemplateSelector x:Key="ParameterSelector"
TextParameterTemplate="{StaticResource TextParameterTemplate}"
ParameterListTemplate="{StaticResource ParameterListTemplate}"/>
</UserControl.Resources>

<ItemsControl ItemsSource="{Binding Parameters}" ItemTemplateSelector="{StaticResource ParameterSelector}"></ItemsControl>


When I run the application, the
TextParameter
in the
MainViewModel.Parameters
are well loaded (
VM.Name
and
VM.Value
properties are well binded to
UC.ParamName
and
UC.Value
. Contrariwise, the
ParameterList
in
MainViewModel.Parameters
are partially loaded.
UC.Name
is well binded to the
UC.ParamName
but the
VM.Parameters
is not binded to the
UC.Items
(the
UC.DataContext
is the VM, the
VM.Parameters
is well defined, but the
UC.Items
is desperately
null
).

Do you have any idea of what I am missing ?
(I am not a native speaker, excuse me if my english hurts you)

Answer

I finally find out:

The Items dependency property of ParameterListControl was a IList<string>. It was a copy/paste mistake from another UC. I changed it to IEnumerable and everything works fine now:

public IEnumerable Items
{
    get { return (IEnumerable)GetValue(ItemsProperty); }
    set { SetValue(ItemsProperty, value); }
}

public static readonly DependencyProperty ItemsProperty =
        DependencyProperty.Register("Items", typeof(IEnumerable), typeof(ParameterListControl), new PropertyMetadata(new List<object>()));

I continued to work on the code and it is now finished and truly composite compared to the sample I posted earlier. If someone is interested in seeing/using this code, you can find it on github.

Comments