使用 DependencyProperty 在 CustomControl 中绑定 ComboBox 的 SelectedItem 的问题

Issues Binding SelectedItem of ComboBox in a CustomControl using DependencyProperty

我正在 WPF 中创建一个自定义控件,其中包含一个文本框、图像按钮和一个组合框。我能够正确布局所有内容,并且所有绑定都适用于除组合框的 SelectedItem 之外的所有内容。

自定义控件代码如下:

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;
        }
    }
}

这是它在主要内容中的引用方式 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}"/>

此 window 的数据上下文是 MainWindowViewModel,它有一个 GelPakPickerViewModel 属性,所有绑定都连接到该模型。 "Location"、"GelPakSource" 和 "SaveCommand" 属性都正常工作,并按照我期望的方式将所有内容路由到 GelPakPickerViewModel。但是,当您 select 组合框中的任何内容时,它实际上从未进入 GelPakViewModels SelectedGelPak 属性(属于 GelPak 类型)。

这是怎么回事?有没有人有解决这个问题的建议?!?

编辑:我向 SelectedGelPakProperty 添加了一个 属性 更改的事件侦听器,如下所示:

    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;
        }
    }

但这仍然没有真正改变视图模型中的 SelectedGelPak 对象。

您没有指定在 SelectedGelPak 更改其值时分配的任何操作 (仅 FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)。 添加

new FrameworkPropertyMetadata(null, OnSelectedGelPakChanged));

并在此方法中将 SelectedGelPak 分配给 gpSelector.SelectedItem

编辑
老实说,您的代码看起来很糟糕,因为您将视觉声明和逻辑都放在一个 class 中。您有 .xaml 文件来声明您的 class 的样子,并且 .xaml.cs 用于某些逻辑。然后将它们分开如下:

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); }
}
}

在这种情况下至关重要的是构造函数。您的 StackPanel 的 DataContext 指向您的代码隐藏文件,因此 StackPanel 中的元素可以毫不费力地访问声明的依赖属性,但整个 GelPakPickerOverlay 的 DataContext 仍然基于父级,因此没有任何改变。试试吧。

SelectedGelPak 绑定应该是双向的。

要么你设置Binding.Mode属性,喜欢

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

或者通过在 属性 元数据中设置相应的标志,使 SelectedGelPak 属性 默认双向绑定:

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

编辑:您现在可以直接将内部 ComboBox 的 SelectedItem 属性 绑定到 SelectedGelPak [=28],而不是使用 PropertyChangedCallback (OnSelectedGelPakChanged) =]:

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

您正在监听 ViewModel 属性 的变化,但这只是数据流动的方式之一。您需要在 Combo 中监听视图的变化。

为此,请像这样订阅其 SelectionChanged 事件:

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

然后在您的事件处理程序中,相应地更改您的 DependencyProperty 的值:

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

这样您就支持了 ViewModel 和 Control 之间的双向通信。