属性 的自定义控件和视图模型绑定已更改

Custom Control and View Model Binding with Property Changed

编辑。说明问题的示例项目,当我选中一个框时,ViewModel 属性 不会更新。 http://1drv.ms/1JZJsNa

我有以下用户控件

是多selectComboBox。要构建此控件,我有以下 XAML:

<UserControl x:Class="GambitFramework.Utilities.Controls.Views.MultiSelectComboBoxView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:MahApps="http://metro.mahapps.com/winfx/xaml/controls">
    <ComboBox x:Name="MultiSelectCombo"
                 SnapsToDevicePixels="True"
                 OverridesDefaultStyle="True"
                 ScrollViewer.HorizontalScrollBarVisibility="Auto"
                 ScrollViewer.VerticalScrollBarVisibility="Auto"
                 ScrollViewer.CanContentScroll="True"
                 IsSynchronizedWithCurrentItem="True">
        <ComboBox.ItemTemplate>
            <DataTemplate>
                <CheckBox Content="{Binding Title}" 
                             IsChecked="{Binding Path=IsSelected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
                             Tag="{RelativeSource FindAncestor, AncestorType={x:Type ComboBox}}"
                             Click="CheckBox_Click"/>
            </DataTemplate>
        </ComboBox.ItemTemplate>
        <ComboBox.Template>
            <ControlTemplate TargetType="ComboBox">
                <Grid>
                    <ToggleButton x:Name="ToggleButton"
                                      Grid.Column="2"
                                      IsChecked="{Binding Path=IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"
                                      Focusable="false"                           
                                      ClickMode="Press" 
                                      HorizontalContentAlignment="Left">
                        <ToggleButton.Template>
                            <ControlTemplate>
                                <Grid>
                                    <Grid.ColumnDefinitions>
                                        <ColumnDefinition Width="*"/>
                                        <ColumnDefinition Width="18"/>
                                    </Grid.ColumnDefinitions>
                                    <Border x:Name="Border" 
                                              Grid.ColumnSpan="2"
                                              CornerRadius="0"
                                              Background="White"
                                              BorderBrush="{DynamicResource TextBoxBorderBrush}"
                                              BorderThickness="1,1,1,1"/>
                                    <Border x:Name="BorderComp" 
                                              Grid.Column="0"
                                              CornerRadius="0" 
                                              Margin="1" 
                                              Background="White"
                                              BorderBrush="{DynamicResource TextBoxBorderBrush}"
                                              BorderThickness="0,0,0,0">
                                        <TextBlock Text="{Binding Path=Text, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}" 
                                         Background="{DynamicResource ControlBackgroundBrush}" 
                                                      Foreground="{DynamicResource TextBrush}"
                                                      FontFamily="{DynamicResource ContentFontFamily}"
                                                      FontSize="{DynamicResource ContentFontSize}"
                                                      FontWeight="Normal"
                                                      HorizontalAlignment="Left"
                                                      VerticalAlignment="Center"
                                                      SnapsToDevicePixels="True"
                                                      Padding="3"/>
                                    </Border>
                                    <Path x:Name="Arrow"
                                            Grid.Column="1"     
                                            IsHitTestVisible="false"
                                 SnapsToDevicePixels="True"
                                 Data="F1 M 301.14,-189.041L 311.57,-189.041L 306.355,-182.942L 301.14,-189.041 Z "
                                 HorizontalAlignment="Center"
                                            VerticalAlignment="Center"
                                 Height="4"
                                 Stretch="Uniform"
                                 Width="8"
                                 Fill="{DynamicResource GrayBrush1}" />
                                </Grid>
                            </ControlTemplate>
                        </ToggleButton.Template>
                    </ToggleButton>
                    <Popup Name="Popup"
                      Placement="Bottom"                        
                      AllowsTransparency="True" 
                      Focusable="False"  IsOpen="{TemplateBinding IsDropDownOpen}"
                      PopupAnimation="Slide">
                        <Grid Name="DropDown"
                        SnapsToDevicePixels="True"  
                        MinWidth="{TemplateBinding ActualWidth}"
                        MaxHeight="{TemplateBinding MaxDropDownHeight}">
                            <Border x:Name="DropDownBorder" 
                             BorderThickness="1" 
                                      Background="White"
                             BorderBrush="{DynamicResource TextBoxBorderBrush}"/>
                            <ScrollViewer Margin="4,6,4,6" 
                                              SnapsToDevicePixels="True" 
                                              DataContext="{Binding}">
                                <StackPanel IsItemsHost="True" 
                                                KeyboardNavigation.DirectionalNavigation="Contained"/>
                            </ScrollViewer>
                        </Grid>
                    </Popup>
                </Grid>
                <ControlTemplate.Triggers>
                    <Trigger Property="HasItems" Value="false">
                        <Setter TargetName="DropDownBorder" Property="MinHeight" Value="95"/>
                    </Trigger>
                    <Trigger SourceName="Popup" 
                                Property="Popup.AllowsTransparency" 
                                Value="true">
                        <Setter TargetName="DropDownBorder" 
                                  Property="CornerRadius" 
                                  Value="0"/>
                        <Setter TargetName="DropDownBorder" 
                                  Property="Margin" 
                                  Value="0,2,0,0"/>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </ComboBox.Template>
    </ComboBox>
</UserControl>

后面的代码有多个DP是:

public partial class MultiSelectComboBoxView : UserControl, INotifyPropertyChanged
{
    private ObservableCollection<ComboBoxNode> nodeList;

    public MultiSelectComboBoxView()
    {
        InitializeComponent();
        nodeList = new ObservableCollection<ComboBoxNode>();
    }

    public static readonly DependencyProperty ShowAllCheckBoxProperty =
         DependencyProperty.Register("ShowAllCheckBox", typeof(bool), 
         typeof(MultiSelectComboBoxView), new PropertyMetadata(true));

    public static readonly DependencyProperty ItemsSourceProperty = 
        DependencyProperty.Register("ItemsSource", typeof(Dictionary<string, object>), 
        typeof(MultiSelectComboBoxView), new FrameworkPropertyMetadata(
            null, new PropertyChangedCallback(MultiSelectComboBoxView.OnItemsSourceChanged)));

    public static readonly DependencyProperty SelectedItemsProperty =
     DependencyProperty.Register("SelectedItems", typeof(Dictionary<string, object>), 
     typeof(MultiSelectComboBoxView), new FrameworkPropertyMetadata(null,
        new PropertyChangedCallback(MultiSelectComboBoxView.OnSelectedItemsChangedCallback)));

    public static readonly DependencyProperty TextProperty =
        DependencyProperty.Register("Text", typeof(string), typeof(MultiSelectComboBoxView), 
        new UIPropertyMetadata(string.Empty));

    public static readonly DependencyProperty DefaultTextProperty =
         DependencyProperty.Register("DefaultText", typeof(string), 
         typeof(MultiSelectComboBoxView), new UIPropertyMetadata(string.Empty));

    public bool ShowAllCheckBox
    {
        get { return (bool)GetValue(ShowAllCheckBoxProperty); }
        set { SetValue(ShowAllCheckBoxProperty, value); }
    }

    public Dictionary<string, object> ItemsSource
    {
        get { return (Dictionary<string, object>)GetValue(ItemsSourceProperty); }
        set { SetValue(ItemsSourceProperty, value); }
    }

    public Dictionary<string, object> SelectedItems
    {
        get { return (Dictionary<string, object>)GetValue(SelectedItemsProperty); }
        set { SetValue(SelectedItemsProperty, value); }
    }

    public string Text
    {
        get { return (string)GetValue(TextProperty); }
        set { SetValue(TextProperty, value); }
    }

    public string DefaultText
    {
        get { return (string)GetValue(DefaultTextProperty); }
        set { SetValue(DefaultTextProperty, value); }
    }

    private static void OnItemsSourceChanged(
        DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        MultiSelectComboBoxView control = (MultiSelectComboBoxView)d;
        control.DisplayInControl();
    }

    private static void OnSelectedItemsChangedCallback(
        DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        MultiSelectComboBoxView control = (MultiSelectComboBoxView)d;
        control.SelectNodes();
        control.SetText();
        control.CheckSetAllSelected();
    }

    private void CheckBox_Click(object sender, RoutedEventArgs e)
    {
        CheckBox clickedBox = (CheckBox)sender;
        if (clickedBox.Content.ToString() == "All")
        {
            if (clickedBox.IsChecked.Value)
            {
                foreach (ComboBoxNode node in nodeList)
                    node.IsSelected = true;
            }
            else
            {
                foreach (ComboBoxNode node in nodeList)
                    node.IsSelected = false;
            }
        }
        else
            CheckSetAllSelected();
        SetSelectedItems();
        SetText();
    }

    private void CheckSetAllSelected()
    {
        int selectedCount = 0;
        foreach (ComboBoxNode s in nodeList)
            if (s.IsSelected && s.Title != "All")
                selectedCount++;
        if (selectedCount == nodeList.Count - 1)
            nodeList.FirstOrDefault(i => i.Title == "All").IsSelected = true;
        else
            nodeList.FirstOrDefault(i => i.Title == "All").IsSelected = false;
    }

    private void SelectNodes()
    {
        foreach (KeyValuePair<string, object> keyValue in SelectedItems)
        {
            ComboBoxNode node = nodeList.FirstOrDefault(i => i.Title == keyValue.Key);
            if (node != null)
                node.IsSelected = true;
        }
    }

    private void SetSelectedItems()
    {
        if (SelectedItems == null)
            SelectedItems = new Dictionary<string, object>();
        SelectedItems.Clear();

        foreach (ComboBoxNode node in nodeList)
        {
            if (node.IsSelected && node.Title != "All")
            {
                if (this.ItemsSource.Count > 0)
                    SelectedItems.Add(node.Title, this.ItemsSource[node.Title]);
            }
        }

        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null)
            handler(this, new PropertyChangedEventArgs("SelectedItems"));
    }

    private void DisplayInControl()
    {
        nodeList.Clear();
        if (this.ShowAllCheckBox && this.ItemsSource.Count > 0)
            nodeList.Add(new ComboBoxNode("All"));
        foreach (KeyValuePair<string, object> keyValue in this.ItemsSource)
        {
            ComboBoxNode node = new ComboBoxNode(keyValue.Key);
            nodeList.Add(node);
        }
        MultiSelectCombo.ItemsSource = nodeList;
    }

    private void SetText()
    {
        if (this.SelectedItems != null)
        {
            StringBuilder displayText = new StringBuilder();
            foreach (ComboBoxNode s in nodeList)
            {
                if (s.IsSelected == true && s.Title == "All")
                {
                    displayText = new StringBuilder();
                    displayText.Append("All");
                    break;
                }
                else if (s.IsSelected == true && s.Title != "All")
                {
                    displayText.Append(s.Title);
                    displayText.Append(", ");
                }
            }
            this.Text = displayText.ToString().TrimEnd().TrimEnd(new char[] { ',' });
        }

        // Set DefaultText if nothing else selected.
        if (string.IsNullOrEmpty(this.Text))
            this.Text = this.DefaultText;
    }
    #endregion // Methods.

    public event PropertyChangedEventHandler PropertyChanged;
}

public class ComboBoxNode : INotifyPropertyChanged
{
    private string title;
    private bool isSelected;

    public ComboBoxNode(string title)
    {
        Title = title;
    }

    public string Title
    {
        get { return title; }
        set
        {
            title = value;
            NotifyPropertyChanged("Title");
        }
    }

    public bool IsSelected
    {
        get { return isSelected; }
        set
        {
            isSelected = value;
            NotifyPropertyChanged("IsSelected");
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
    protected void NotifyPropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}

}

支持 ViewModel

public class MultiSelectComboBoxViewModel : PropertyChangedBase
{
    private Dictionary<string, object> items;
    private Dictionary<string, object> selectedItems;

    public MultiSelectComboBoxViewModel() { }
    public MultiSelectComboBoxViewModel(Dictionary<string, object> items)
    {
        Items = items;
        SelectedItems = new Dictionary<string, object>();
    }
    public MultiSelectComboBoxViewModel(Dictionary<string, object> items,
        Dictionary<string, object> selectedItems)
    {
        Items = items;
        SelectedItems = selectedItems;
    }

    public Dictionary<string, object> Items
    {
        get { return items; }
        set
        {
            if (items == value)
                return;
            items = value;
            NotifyOfPropertyChange(() => Items);
        }
    }

    public Dictionary<string, object> SelectedItems
    {
        get { return selectedItems; }
        set
        {
            if (selectedItems == value)
                return;
            selectedItems = value;
            NotifyOfPropertyChange(() => SelectedItems);
        }
    }
}

现在,我把这个控件放在我的一些视图中,我们称之为 SomeView.xaml

<Controls:MultiSelectComboBoxView  
    ...
    ItemsSource="{Binding SelectionMultiFilter.Items}"
    SelectedItems="{Binding SelectionMultiFilter.SelectedItems}"/>

在我的 SomeViewModel.cs 我有

public MultiSelectComboBoxViewModel SelectionMultiFilter { get; set; }

并通过 MultiSelectComboBoxViewModel 的构造函数填充项目,所有项目都会填充,我可以通过 SelectedItems 属性 获得那些 selected。现在我的问题是,在 SomeViewModel 中,我似乎无法订阅 SelectionMultiFilterPropertyChanged 事件,它不会触发。现在我做了显而易见的事情并将我的 DP 更改为包含

public static readonly DependencyProperty SelectedItemsProperty =
    DependencyProperty.Register("SelectedItems", typeof(Dictionary<string, object>), 
        typeof(MultiSelectComboBoxView), new FrameworkPropertyMetadata(null,
        new PropertyChangedCallback(MultiSelectComboBoxView.OnSelectedItemsChangedCallback)));

public Dictionary<string, object> SelectedItems
{
    get { return (Dictionary<string, object>)GetValue(SelectedItemsProperty); }
    set { SetValue(SelectedItemsProperty, value); }
}       

private static void OnSelectedItemsChangedCallback(
    DependencyObject o, DependencyPropertyChangedEventArgs e)
{
    MultiSelectComboBoxView control = (MultiSelectComboBoxView)d;
    control.SelectNodes();
    control.SetText();
    control.CheckSetAllSelected();

    MultiSelectComboBoxView d = o as MultiSelectComboBoxView;
    if (d != null) {
        d.OnSelectedItemsChanged();
    }
}

protected virtual void OnSelectedItemsChanged() {
    OnPropertyChanged("SelectedItems");
}

但这并没有激发我的 PropertyChanged。问题:

  1. 当我的 MultiSelectComboBoxSelectedItems 发生变化时,如何让我的消费 ViewModel 得到通知?

  2. 如果我没有在 MultiSelectComboBox 的 ctor 中设置 SelectedItems 当我之后从消费视图模型中设置它们时,它们不会更新,如何我要解决这个问题吗?

感谢您的宝贵时间。

我认为依赖项 属性 setter 没有触发 属性 更改通知。

public Dictionary<string, object> SelectedItems
{
    get { return (Dictionary<string, object>)GetValue(SelectedItemsProperty);}
    set { SetValue(SelectedItemsProperty, value); NotifyOfPropertyChange(() => Items);}
}

但无论如何我无法调查完整的代码,当你把断点放在这里时:

public Dictionary<string, object> SelectedItems
{
    get { return selectedItems; }
    set
    {
        if (selectedItems == value) <----------- here breakpoint
            return;
        selectedItems = value;
        NotifyOfPropertyChange(() => SelectedItems);
    }
}

更改一个复选框后应用程序会在那里中断吗?如果不是,看起来您正在管理集合本身,因此您应该在集合更改时通知更改(实施 ObservableDictionary)