Search code examples
c#wpfcustom-controlsdependency-properties

WPF Custom Control, Combine multiple ComboBox values into single DependencyProperty


I have a custom control with two ComboBoxes with hard coded ComboBoxItems in them and I would like to be able to bind to the values via a single DependencyProperty but can't seem to get the bindings working. Maybe my approach is way off, but how should I do to get it working?

Generic.xaml:

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:MyApp.Components">
    <Style TargetType="{x:Type local:ContractLimitSelector}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:ContractLimitSelector}">
                    <ControlTemplate.Resources>
                        <BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
                    </ControlTemplate.Resources>
                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="auto" />
                            <ColumnDefinition Width="auto" />
                        </Grid.ColumnDefinitions>
                        <ComboBox x:Name="PART_ComboAA" Grid.Column="0" SelectedValue="{TemplateBinding SelectedAA}" />
                        <ComboBox x:Name="PART_ComboFA" Grid.Column="1" SelectedValue="{TemplateBinding SelectedFA}" />
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

Custom Control:

public class SelectorItems
{
    public string AA { get; set; }
    public string FA { get; set; }
}

public class ContractLimitSelector : Control
{
    private static List<string> ComboBoxSource = new List<string>()
    {
        "-",
        "BE",
        "EE",
    };

    static ContractLimitSelector()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(ContractLimitSelector), new FrameworkPropertyMetadata(typeof(ContractLimitSelector)));
    }

    public override void OnApplyTemplate()
    {
        var comboAA = Template.FindName("PART_ComboAA", this) as ComboBox;
        var comboFA = Template.FindName("PART_ComboFA", this) as ComboBox;

        comboAA.ItemsSource = ComboBoxSource;
        comboFA.ItemsSource = ComboBoxSource;

        base.OnApplyTemplate();
    }

    private SelectorItems GetAllValues()
    {
        var AA = (string)GetValue(SelectedAAProperty);
        var FA = (string)GetValue(SelectedFAProperty);

        return new SelectorItems() { AA = AA, FA = FA };
    }

    public static readonly DependencyProperty VisibleHeaderProperty =
        DependencyProperty.Register("VisibleHeader", typeof(bool), typeof(ContractLimitSelector),
            new PropertyMetadata(false));

    public bool VisibleHeader
    {
        get { return (bool)GetValue(VisibleHeaderProperty); }
        set { SetValue(VisibleHeaderProperty, value); }
    }

    public static readonly DependencyProperty SelectedAAProperty =
        DependencyProperty.Register("SelectedAA", typeof(string), typeof(ContractLimitSelector),
            new PropertyMetadata(string.Empty));

    public string SelectedAA
    {
        get { return (string)GetValue(SelectedAAProperty); }
        set
        {
            var all = GetAllValues();
            SetValue(SelectedValuesProperty, new SelectorItems() { AA = value, FA = all.FA, });
            SetValue(SelectedAAProperty, value);
        }
    }

    public static readonly DependencyProperty SelectedFAProperty =
        DependencyProperty.Register("SelectedFA", typeof(string), typeof(ContractLimitSelector),
            new PropertyMetadata(string.Empty));

    public string SelectedFA
    {
        get { return (string)GetValue(SelectedFAProperty); }
        set
        {
            var all = GetAllValues();
            SetValue(SelectedValuesProperty, new SelectorItems() { AA = all.AA, FA = value, });
            SetValue(SelectedFAProperty, value);
        }
    }

    public SelectorItems SelectedValues
    {
        get
        {
            return new SelectorItems()
            {
                AA = (string)GetValue(SelectedAAProperty),
                FA = (string)GetValue(SelectedFAProperty),
            };
        }
        set
        {
            SetValue(SelectedAAProperty, value.AA);
            SetValue(SelectedFAProperty, value.FA);
        }
    }

    public static readonly DependencyProperty SelectedValuesProperty =
        DependencyProperty.Register("SelectedValues", typeof(SelectorItems), typeof(ContractLimitSelector),
            new PropertyMetadata(new SelectorItems()));
}

I use it like this: XAML:

<custom:ContractLimitSelector SelectedValues="{Binding PrepareLockHouseValue, Mode=TwoWay}" />

ViewModel:

private SelectorItems _contractValues;
public SelectorItems ContractValues
{
    get
    {
        return _contractValues;
    }
    set
    {
        _contractValues = value;
        OnPropertyChanged(nameof(ContractValues));
    }
}

I thought using SetValue whenever any of the ComboBoxes change would trigger the "shared" SelectedValues DependecyProperty, but nothing ever gets triggered...

What am I missing?


Solution

  • The SelectedAA and SelectedFA property setters are not called when you call

    SetValue(SelectedAAProperty, value.AA);
    SetValue(SelectedFAProperty, value.FA);
    

    You would have to write

    SelectedAA = value.AA;
    SelectedFA = value.FA;
    

    Better move all code except

    SetValue(SelectedAAProperty, value);
    

    and

    SetValue(SelectedFAProperty, value);
    

    out of the setters and add PropertyChangedCallbacks to execute the other code.

    The properties would then also properly work with data bindings and other possible value sources.


    In general, the get and set methods of the CLR wrapper of a dependency property must not contain anything else than the GetValue and SetValue calls, because get/set may be bypassed by the framework.

    See XAML Loading and Dependency Properties for details.