简体   繁体   中英

Custom Control and View Model Binding with Property Changed

Edit. Example project that illustrates the problem, when I check a box, the ViewModel property does not update. http://1drv.ms/1JZJsNa

I have the following user control

multicombo

it is a multi-select ComboBox . To construct this control, I have the following 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>

The code behind has a number of DPs and is:

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

}

with the backing 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);
        }
    }
}

Now, I put this control in my some view, lets call it SomeView.xaml

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

and in my SomeViewModel.cs I have

public MultiSelectComboBoxViewModel SelectionMultiFilter { get; set; }

and populate the items via the ctor for the MultiSelectComboBoxViewModel , all items populate and I can get those selected via the SelectedItems property. Now my problem is that in SomeViewModel I cannot seem to subscribe to the PropertyChanged event for SelectionMultiFilter , it does not fire. Now I have done the obvious thing and changed my DP to include

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

But this does not fire my PropertyChanged . Questions:

  1. How can I get my consuming ViewModel to be notified when the SelectedItems of my MultiSelectComboBox get changed?

  2. If I don't set the SelectedItems in the ctor for the MultiSelectComboBox when I set them from the consuming view model after, they don't update, how do I fix this?

Thanks for your time.

I think dependecy property setter is not firing property changed notification.

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

But anyway I am not able to investigate the code complete, when you put the breakpoint here:

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

will application break there after the one checkbox is changed? If not it looks that you are managing the collection as itself, so you should notified the change when collection is changed (implement ObservableDictionary)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM