Reputation: 23833
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
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:
How can I get my consuming ViewModel to be notified when the SelectedItems
of my MultiSelectComboBox
get changed?
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.
Upvotes: 1
Views: 1577
Reputation: 148
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)
Upvotes: 1