kformeck
kformeck

Reputation: 1823

Issues Binding SelectedItem of ComboBox in a CustomControl using DependencyProperty

I am creating a custom control in WPF that contains a text box, image button and a combo box. I am able to get everything to layout correctly and all the bindings work with everything except for the SelectedItem of the combo box.

Here is the custom control code:

public class GelPakPickerOverlay : Border
{
    public static readonly DependencyProperty SelectedGelPakProperty =
        DependencyProperty.Register(
            "SelectedGelPak",
            typeof(object),
            typeof(GelPakPickerOverlay),
            new FrameworkPropertyMetadata(
                null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
    public static readonly DependencyProperty LocationProperty =
        DependencyProperty.Register(
            "Location",
            typeof(string),
            typeof(GelPakPickerOverlay),
            new FrameworkPropertyMetadata(string.Empty, OnLocationChanged));
    public static readonly DependencyProperty GelPakSourceProperty =
        DependencyProperty.Register(
            "GelPakSource",
            typeof(IEnumerable),
            typeof(GelPakPickerOverlay),
            new FrameworkPropertyMetadata(null, OnGelPakSourceChanged));
    public static readonly DependencyProperty SaveCommandProperty =
        DependencyProperty.Register(
            "SaveCommand",
            typeof(ICommand),
            typeof(GelPakPickerOverlay),
            new FrameworkPropertyMetadata(null, OnSaveCommandChanged));
    private static ComboBox gpSelector;
    private static TextBox gpLocation;
    private static Button saveButton;

    public GelPakPickerOverlay()
    {
        Height = 98;

        gpSelector = new ComboBox();
        gpSelector.Width = 100;
        gpSelector.Margin = new Thickness(10);
        gpSelector.HorizontalAlignment = HorizontalAlignment.Left;
        gpSelector.VerticalAlignment = VerticalAlignment.Center;

        Grid grid = new Grid();
        grid.ColumnDefinitions.Add(new ColumnDefinition());
        ColumnDefinition def = new ColumnDefinition();
        def.Width = new GridLength(40);
        grid.ColumnDefinitions.Add(def);

        gpLocation = new TextBox();
        gpLocation.Style = (Style) FindResource("TextBoxStyleBase");
        gpLocation.Width = 70;
        gpLocation.Margin = new Thickness(10);
        gpLocation.HorizontalAlignment = HorizontalAlignment.Left;
        gpLocation.VerticalAlignment = VerticalAlignment.Center;
        Grid.SetColumn(gpLocation, 0);

        saveButton = new Button();
        saveButton.Style = (Style) FindResource("SaveButton");
        saveButton.Margin = new Thickness(0, 10, 10, 10);
        saveButton.HorizontalAlignment = HorizontalAlignment.Center;
        Grid.SetColumn(saveButton, 1);

        grid.Children.Add(gpLocation);
        grid.Children.Add(saveButton);

        StackPanel mainChild = new StackPanel();
        mainChild.Orientation = Orientation.Vertical;
        mainChild.Children.Add(gpSelector);
        mainChild.Children.Add(grid);

        Child = mainChild;
    }

    public object SelectedGelPak
    {
        get { return GetValue(SelectedGelPakProperty); }
        set { SetValue(SelectedGelPakProperty, value); }
    }
    public string Location
    {
        get { return GetValue(LocationProperty).ToString(); }
        set { SetValue(LocationProperty, value); }
    }
    public IEnumerable GelPakSource
    {
        get { return (IEnumerable) GetValue(GelPakSourceProperty); }
        set { SetValue(GelPakSourceProperty, value); }
    }
    public ICommand SaveCommand
    {
        get { return (ICommand) GetValue(SaveCommandProperty); }
        set { SetValue(SaveCommandProperty, value); }
    }

    private static void OnLocationChanged(
        DependencyObject source, DependencyPropertyChangedEventArgs e)
    {
        if (gpLocation != null)
        {
            gpLocation.Text = e.NewValue.ToString();
        }
    }
    private static void OnGelPakSourceChanged(
        DependencyObject source, DependencyPropertyChangedEventArgs e)
    {
        if (gpSelector != null)
        {
            gpSelector.ItemsSource = (IEnumerable) e.NewValue;
        }
    }
    private static void OnSaveCommandChanged(
        DependencyObject source, DependencyPropertyChangedEventArgs e)
    {
        if (saveButton != null)
        {
            saveButton.Command = (ICommand) e.NewValue;
        }
    }
}

This is how it is referenced in the main window:

    <ctl:GelPakPickerOverlay
        Width="132"
        DockPanel.Dock="Right"
        VerticalAlignment="Bottom"
        Background="{StaticResource primaryBrush}"
        BorderBrush="{StaticResource accentBrushOne}"
        BorderThickness="2,2,0,0"
        Visibility="{
            Binding GelPakPickerViewModel.IsPickerVisible,
            Converter={StaticResource BoolToHiddenVisConverter},
            FallbackValue=Visible}"
        GelPakSource="{Binding GelPakPickerViewModel.GelPakList}"
        SelectedGelPak="{Binding GelPakPickerViewModel.SelectedGelPak}"
        Location="{Binding GelPakPickerViewModel.GelPakLocation, UpdateSourceTrigger=LostFocus}"
        SaveCommand="{Binding GelPakPickerViewModel.UpdateGpDataCommand}"/>

The data context of this window is MainWindowViewModel which has a GelPakPickerViewModel property which all of the bindings are hooked up to. The "Location", "GelPakSource" and "SaveCommand" properties all work correctly and routes everything to the GelPakPickerViewModel the way I expect. However, when you select anything from the combo box, it never actually makes it into the GelPakViewModels SelectedGelPak property (which is of type GelPak).

What is going on here? Does anyone have any suggestions to fix this issue?!?

EDIT: I added a property changed event listener to the SelectedGelPakProperty like this:

    public static readonly DependencyProperty SelectedGelPakProperty =
        DependencyProperty.Register(
            "SelectedGelPak",
            typeof(object),
            typeof(GelPakPickerOverlay),
            new FrameworkPropertyMetadata(null, OnSelectedGelPakChanged));

    ........

    private static void OnSelectedGelPakChanged(
        DependencyObject source, DependencyPropertyChangedEventArgs e)
    {
        if (gpLocation != null)
        {
            gpSelector.SelectedItem = e.NewValue;
        }
    }

but this still doesn't actually change the SelectedGelPak object in the view model.

Upvotes: 1

Views: 1322

Answers (3)

almulo
almulo

Reputation: 4978

You're listening to changes on the property of your ViewModel, but that's just one of the ways in which the data is flowing. You need to listen to changes in the view, in your Combo.

To do so, subscribe to its SelectionChanged event like this:

gpSelector = new ComboBox();
gpSelector.Width = 100;
gpSelector.Margin = new Thickness(10);
gpSelector.HorizontalAlignment = HorizontalAlignment.Left;
gpSelector.VerticalAlignment = VerticalAlignment.Center;
gpSelector.SelectionChanged += OnGpSelectorSelectionChanged;

And then in your event handler, change the value of your DependencyProperty accordingly:

private void OnGpSelectorSelectionChanged(object sender, SelectionChangedEventArgs e)
{
    SetCurrentValue(SelectedGelPakProperty, gpSelector.SelectedItem);
}

This way you're supporting the two-way communication between ViewModel and Control.

Upvotes: 2

Clemens
Clemens

Reputation: 128060

The SelectedGelPak binding should be two-way.

Either you set the Binding.Mode property, like

SelectedGelPak="{Binding GelPakPickerViewModel.SelectedGelPak, Mode=TwoWay}"

or you make the SelectedGelPak property bind two-way by default, by setting the corresponding flag in the property metadata:

public static readonly DependencyProperty SelectedGelPakProperty =
    DependencyProperty.Register(
        "SelectedGelPak",
        typeof(object),
        typeof(GelPakPickerOverlay),
        new FrameworkPropertyMetadata(
            null,
            FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)); // here

EDIT: Instead of having a PropertyChangedCallback (OnSelectedGelPakChanged), you may now directly bind the SelectedItem property of the internal ComboBox to the SelectedGelPak property:

<ComboBox ... SelectedItem="{Binding SelectedGelPak,
    RelativeSource={RelativeSource AncestorType=local:GelPakPickerOverlay}}"/>

Upvotes: 1

Maximus
Maximus

Reputation: 3448

You do not specify any action to assign when SelectedGelPak changes its value (only FrameworkPropertyMetadataOptions.BindsTwoWayByDefault). Add

new FrameworkPropertyMetadata(null, OnSelectedGelPakChanged));

and in this method assign SelectedGelPak to gpSelector.SelectedItem

EDIT:
Honestly, your code looks nasty since you placed in one class both visual declaration and logic. You have .xaml file to declare how your class looks like and .xaml.cs for some logic. Separate then them as follows:

XAML:

<StackPanel Name="MainPanel">
    <ComboBox SelectedItem="{Binding SelectedGelPak}"
              ItemsSource="{Binding GelPakSource}"/>
    <TextBox Text="{Binding Location}"/>
    <Button Command="{Binding SaveCommand}"/>
</StackPanel>

.XAML.CS:

  public static readonly DependencyProperty SelectedGelPakProperty =
    DependencyProperty.Register(
        "SelectedGelPak",
        typeof(object),
        typeof(GelPakPickerOverlay),
        new FrameworkPropertyMetadata(
            null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public static readonly DependencyProperty LocationProperty =
    DependencyProperty.Register(
        "Location",
        typeof(string),
        typeof(GelPakPickerOverlay),
        new FrameworkPropertyMetadata(string.Empty, OnLocationChanged));
public static readonly DependencyProperty GelPakSourceProperty =
    DependencyProperty.Register(
        "GelPakSource",
        typeof(IEnumerable),
        typeof(GelPakPickerOverlay),
        new FrameworkPropertyMetadata(null, OnGelPakSourceChanged));
public static readonly DependencyProperty SaveCommandProperty =
    DependencyProperty.Register(
        "SaveCommand",
        typeof(ICommand),
        typeof(GelPakPickerOverlay),
        new FrameworkPropertyMetadata(null, OnSaveCommandChanged));

public GelPakPickerOverlay()
{
    this.MainPanel.DataContext = this;
}

public object SelectedGelPak
{
    get { return GetValue(SelectedGelPakProperty); }
    set { SetValue(SelectedGelPakProperty, value); }
}
public string Location
{
    get { return GetValue(LocationProperty).ToString(); }
    set { SetValue(LocationProperty, value); }
}
public IEnumerable GelPakSource
{
    get { return (IEnumerable) GetValue(GelPakSourceProperty); }
    set { SetValue(GelPakSourceProperty, value); }
}
public ICommand SaveCommand
{
    get { return (ICommand) GetValue(SaveCommandProperty); }
    set { SetValue(SaveCommandProperty, value); }
}
}

Crucial in this case is constructor. Your StackPanel's DataContext points to your code-behind file so elements within StackPanel have effortless access to declared Dependency Properties but in turn DataContext of the whole GelPakPickerOverlay still bases on parent's so nothing has changed. Try it.

Upvotes: 0

Related Questions