Umair Farooq Umair Farooq - 3 months ago 35
C# Question

Make Combo Box ItemsSource generic inside of UserControl to make it reusable for Collections of Multiple types

I am working on a

WPF
application and recently came across a requirement of making reusable
User Controls
.

I have two
User Controls
InputUC
and
ComboBoxUC
. Both have
Label
with
TextBox
and
Label
with
ComboBox
respectively. I have successfully implemented the
InputUC
by defining required Dependency Properties.

The problem I am facing is in
ComboBoxUC
. I have a scenario in my application where I have to show
Collection
of
Cities
,
Customers
,
Salesmen
and some other entities in different places. Obviously each entity will offer different property names to
DisplayMemberPath
,
SelectedValuePath
,
SelectedValue
properties and different type of
Collection
as
ItemsSource
property of
ComboBox
.

I have searched on internet as well but didn't find any same solution.

The code I am trying is

ComboBox
control in
ComboBoxUC.xaml


<ComboBox Name="valuesComboBox"
Grid.Column="1"
ItemsSource="{Binding ComboBoxItems}"
DisplayMemberPath="{Binding ComboBoxDisplayMemberPath}"
SelectedValuePath="{Binding ComboBoxSelectedValuePath}"
SelectedValue="{Binding ComboBoxValue}"
IsEnabled="{Binding ComboBoxIsEnabled}"
Style="{StaticResource ComboBox-Base}">
</ComboBox>


Code behind of
ComboBoxUC
in
ComboBoxUC.xaml.cs


public string ComboBoxLabel
{
get { return (string)GetValue(LabelProperty); }
set { SetValue(LabelProperty, value); }
}

public bool ComboBoxIsRequired
{
get { return (bool)GetValue(IsRequiredProperty); }
set { SetValue(IsRequiredProperty, value); }
}

public long ComboBoxValue
{
get { return (long)GetValue(ValueProperty); }
set { SetValue(ValueProperty, value); }
}

public bool ComboBoxIsEnabled
{
get { return (bool)GetValue(ValueEnabledProperty); }
set { SetValue(ValueEnabledProperty, value); }
}

public ObservableCollection<CityViewModel> ComboBoxItems
{
get { return (ObservableCollection<CityViewModel>)GetValue(ValueItems); }
set { SetValue(ValueItems, value); }
}

public string ComboBoxDisplayMemberPath
{
get { return GetValue(ValueDisplayMemberPath).ToString(); }
set { SetValue(ValueDisplayMemberPath, value); }
}

public string ComboBoxSelectedValuePath
{
get { return GetValue(ValueSelectedValuePath).ToString(); }
set { SetValue(ValueSelectedValuePath, value); }
}

public static readonly DependencyProperty LabelProperty = DependencyProperty.Register("ComboBoxLabel", typeof(string),
typeof(ComboBoxUC), new PropertyMetadata(string.Empty));

public static readonly DependencyProperty IsRequiredProperty = DependencyProperty.Register("ComboBoxIsRequired", typeof(bool),
typeof(ComboBoxUC), new PropertyMetadata(false));

public static readonly DependencyProperty ValueProperty = DependencyProperty.Register("ComboBoxValue", typeof(long),
typeof(ComboBoxUC), new FrameworkPropertyMetadata { BindsTwoWayByDefault = true, DefaultUpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged });

public static readonly DependencyProperty ValueEnabledProperty = DependencyProperty.Register("ComboBoxIsEnabled", typeof(bool),
typeof(ComboBoxUC), new FrameworkPropertyMetadata { BindsTwoWayByDefault = false, DefaultUpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged });

public static readonly DependencyProperty ValueItems = DependencyProperty.Register("ComboBoxItems", typeof(ObservableCollection<CityViewModel>),
typeof(ComboBoxUC), new FrameworkPropertyMetadata { BindsTwoWayByDefault = false, DefaultUpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged });

public static readonly DependencyProperty ValueDisplayMemberPath = DependencyProperty.Register("ComboBoxDisplayMemberPath", typeof(string),
typeof(ComboBoxUC), new FrameworkPropertyMetadata { BindsTwoWayByDefault = false, DefaultUpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged });

public static readonly DependencyProperty ValueSelectedValuePath = DependencyProperty.Register("ComboBoxSelectedValuePath", typeof(string),
typeof(ComboBoxUC), new FrameworkPropertyMetadata { BindsTwoWayByDefault = true, DefaultUpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged });

public ComboBoxUC()
{
InitializeComponent();
}


The code from the
Page
control where I am using this
UserControl
is

<local:ComboBoxUC ComboBoxLabel="City"
ComboBoxIsRequired="True"
ComboBoxValue="{Binding CustomerViewModel.customer_city_id}"
ComboBoxItems="{Binding Cities}"
ComboBoxDisplayMemberPath="city_name"
ComboBoxSelectedValuePath="city_id"
ComboBoxIsEnabled="{Binding Flags.AddOrUpdate}">
</local:ComboBoxUC>


Now I will use same above
xaml
in multiple places in my application.
The things that can vary in each case are:


  • CustomerViewModel.customer_city_id

  • Cities

  • city_name

  • city_id



I have correctly set the
DataContext
in
ComboBoxUC.xaml
and the current code for my
UserControl
works correctly for one type of
Collection (CityViewModel)
. I want to use the same code for other entities like
CustomerViewModel
,
SalesmanViewModel
etc. with obviously different property names.

I want the following code to be generic.

public ObservableCollection<CityViewModel> ComboBoxItems
{
get { return (ObservableCollection<CityViewModel>)GetValue(ValueItems); }
set { SetValue(ValueItems, value); }
}

public static readonly DependencyProperty ValueItems = DependencyProperty.Register("ComboBoxItems", typeof(ObservableCollection<CityViewModel>),
typeof(ComboBoxUC), new FrameworkPropertyMetadata { BindsTwoWayByDefault = false, DefaultUpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged });


I have tried
Collection
of
object
type as well but of course
object
type does not have any properties that I have in my entities.

Help will be appreciated as I am stuck and not able to move forward with the development from this point.

Answer

Instead of attempting to make the collections in the user control ore strongly-typed by making them generic, you should instead make them less strongly-typed; remember, the ItemsSource of the Combobox itself is simply of type 'object'.

I would suggest that you make your ComboBoxUC expose a DependencyProperty of type IEnumerable and bind that to the ComboBox ItemsSource. Then also expose a DependencyProperty of type DataTemplate and bind that to the ComboBox's ItemTemplate property. When using the user control you can then provide a simple DataTemplate to display the desired property instead of using DisplayMember path. For example, when you want to display City's in the ComboboxUC, you could do this:

<local:ComboBoxUC ItemsSource="{Binding Cities}">
  <local.ComboBoxUC.ItemTemplate>
    <DataTemplate>
      <TextBlock Text="{Binding city_name}"/>
    </DataTemplate>
  </local:ComboBoxUC.ItemTemplate>
</local:ComboBoxUC/>

I would then expose the ComboBox's SelectedItem as a DependencyProperty of the user control and, if you absolutely must bind to the SelectedValuePath instead of the SelectedItem, use a ValueConverter.

To be honest, it feels like these user controls are a bit OTT. If all you're gaining is a label and some styling, the same could be achieved with retemplating the control in a resource dictionary and applying the template to each combobox you want to use in this manner.