I have a custom combobox in my WPF application and the combo box is not updating when my item source is updated after initial launch. The itemsSource is a custom observableDictionary. I've implemented INotifyPropertyChanged on my properties with my observableDictionary but it won't update. I've searched through every WPF propertychanged event not working on stack overflow and i'm looking for some assistance.
Custom Combobox Code:
<controls:MultiSelectComboBox Width="100" Height="30" Padding="0 10 0 0" Grid.Row="0" Grid.Column="1"
ItemsSource="{Binding Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged,
ValidatesOnDataErrors=True,
Path=DataContext.OptionTeamMembersDictionary,
BindsDirectlyToSource=True,
RelativeSource={RelativeSource AncestorType={x:Type UserControl},Mode=FindAncestor}}"
SelectedItems="{Binding SelectedOptionTeamMembersDictionary, Mode=TwoWay}"
x:Name="TeamMemberDisplay"
ToolTip="{Binding Path=Text, RelativeSource={RelativeSource Self}}"/>
Properties and private variables:
private ObservableDictionary<string, object> _optionTeamMembersDictionary;
private ObservableDictionary<string, object> _selectedOptionTeamMembersDictionary;
public ObservableDictionary<string, object> OptionTeamMembersDictionary
{
get
{
return _optionTeamMembersDictionary;
}
set
{
_optionTeamMembersDictionary = value;
OnPropertyChanged("OptionTeamMembersDictionary");
}
}
public ObservableDictionary<string, object> SelectedOptionTeamMembersDictionary
{
get
{
return _selectedOptionTeamMembersDictionary;
}
set
{
_selectedOptionTeamMembersDictionary = value;
OnPropertyChanged("SelectedOptionTeamMembersDictionary");
}
}
I use a button to trigger a database pull which leads each row into an object and then populates the optionTeamMemberDictionary when it returns more then one row of data.
All of the above works when the data is loaded in my constructor but when its loaded after launch my comboboxes do not show the new data in the collection.
EDIT:
Code for the Custom ComboBox being used:
https://www.codeproject.com/Articles/563862/Multi-Select-ComboBox-in-WPF is the URL this came from. Some edits were made to implement an obserableDirctionary instead of normal dictionary
public partial class MultiSelectComboBox : UserControl
{
private ObservableCollection<Node> _nodeList;
public MultiSelectComboBox()
{
InitializeComponent();
_nodeList = new ObservableCollection<Node>();
}
#region Dependency Properties
public static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register("ItemsSource", typeof(ObservableDictionary<string, object>), typeof(MultiSelectComboBox), new FrameworkPropertyMetadata(null,
new PropertyChangedCallback(MultiSelectComboBox.OnItemsSourceChanged)));
public static readonly DependencyProperty SelectedItemsProperty =
DependencyProperty.Register("SelectedItems", typeof(ObservableDictionary<string, object>), typeof(MultiSelectComboBox), new FrameworkPropertyMetadata(null,
new PropertyChangedCallback(MultiSelectComboBox.OnSelectedItemsChanged)));
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register("Text", typeof(string), typeof(MultiSelectComboBox), new UIPropertyMetadata(string.Empty));
public static readonly DependencyProperty DefaultTextProperty =
DependencyProperty.Register("DefaultText", typeof(string), typeof(MultiSelectComboBox), new UIPropertyMetadata(string.Empty));
public ObservableDictionary<string, object> ItemsSource
{
get { return (ObservableDictionary<string, object>)GetValue(ItemsSourceProperty); }
set
{
SetValue(ItemsSourceProperty, value);
}
}
public ObservableDictionary<string, object> SelectedItems
{
get { return (ObservableDictionary<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); }
}
#endregion
#region Events
private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
MultiSelectComboBox control = (MultiSelectComboBox)d;
control.DisplayInControl();
}
private static void OnSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
MultiSelectComboBox control = (MultiSelectComboBox)d;
control.SelectNodes();
control.SetText();
}
private void CheckBox_Click(object sender, RoutedEventArgs e)
{
CheckBox clickedBox = (CheckBox)sender;
if (clickedBox.Content == "All")
{
if (clickedBox.IsChecked.Value)
{
foreach (Node node in _nodeList)
{
node.IsSelected = true;
}
}
else
{
foreach (Node node in _nodeList)
{
node.IsSelected = false;
}
}
}
else
{
int _selectedCount = 0;
foreach (Node 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;
}
SetSelectedItems();
SetText();
}
#endregion
#region Methods
private void SelectNodes()
{
foreach (KeyValuePair<string, object> keyValue in SelectedItems)
{
Node node = _nodeList.FirstOrDefault(i => i.Title == keyValue.Key);
if (node != null)
node.IsSelected = true;
}
}
private void SetSelectedItems()
{
if (SelectedItems == null)
SelectedItems = new ObservableDictionary<string, object>();
SelectedItems.Clear();
foreach (Node node in _nodeList)
{
if (node.IsSelected && node.Title != "All")
{
if (this.ItemsSource.Count > 0)
SelectedItems.Add(node.Title, this.ItemsSource[node.Title]);
}
}
}
private void DisplayInControl()
{
_nodeList.Clear();
if (this.ItemsSource.Count > 0)
_nodeList.Add(new Node("All"));
foreach (KeyValuePair<string, object> keyValue in this.ItemsSource)
{
Node node = new Node(keyValue.Key);
_nodeList.Add(node);
}
MultiSelectCombo.ItemsSource = _nodeList;
}
private void SetText()
{
if (this.SelectedItems != null)
{
StringBuilder displayText = new StringBuilder();
foreach (Node 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(new char[] { ',' });
}
// set DefaultText if nothing else selected
if (string.IsNullOrEmpty(this.Text))
{
this.Text = this.DefaultText;
}
}
#endregion
}
public class Node : INotifyPropertyChanged
{
private string _title;
private bool _isSelected;
#region ctor
public Node(string title)
{
Title = title;
}
#endregion
#region Properties
public string Title
{
get
{
return _title;
}
set
{
_title = value;
NotifyPropertyChanged("Title");
}
}
public bool IsSelected
{
get
{
return _isSelected;
}
set
{
_isSelected = value;
NotifyPropertyChanged("IsSelected");
}
}
#endregion
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
After much troubleshooting i was able to figure out an answer for this. onItemsSourceChanged was not firing after the control was instantiated so it never got updates made after initial launch of the application. I edited the OnItemsSourceChanged Function on the MultiFunctionComboBox to the below so it would fire on a changed event and it is working as expected now.
private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
MultiSelectComboBox control = (MultiSelectComboBox)d;
var action = new NotifyCollectionChangedEventHandler(
(o, args) =>
{
MultiSelectComboBox c = (MultiSelectComboBox)d;
c?.DisplayInControl();
});
if (e.OldValue != null)
{
var sourceCollection = (ObservableDictionary<string, object>)e.OldValue;
sourceCollection.CollectionChanged -= action;
}
if (e.NewValue != null)
{
var sourceCollection = (ObservableDictionary<string, object>)e.NewValue;
sourceCollection.CollectionChanged += action;
}
control.DisplayInControl();
}
Related
Since CaretIndex of TextBox control is not a DependencyProperty, how do I get the value of CaretIndex in the ViewModel?
You can you Interaction.Behaviors create your own behaviour and get the property you want from it.
For eg. this is a EventCommandConverterBehavior. Using it will return the property value which you specify from xaml, and if property name is not set. it will return Corresponding EventArgs
public class EventCommandConverterBehavior : Behavior<FrameworkElement>
{
private Delegate eventHandler;
private EventInfo oldEvent;
// Event
public string EventName { get { return (string)GetValue(EventNameProperty); } set { SetValue(EventNameProperty, value); } }
public static readonly DependencyProperty EventNameProperty = DependencyProperty.Register("EventName", typeof(string), typeof(EventCommandConverterBehavior), new PropertyMetadata(null, OnEventChanged));
// Command
public ICommand Command { get { return (ICommand)GetValue(CommandProperty); } set { SetValue(CommandProperty, value); } }
public static readonly DependencyProperty CommandProperty = DependencyProperty.Register("Command", typeof(ICommand), typeof(EventCommandConverterBehavior), new PropertyMetadata(null));
public string PropertyName { get { return (string)GetValue(PropertyNameProperty); } set { SetValue(PropertyNameProperty, value); } }
public static readonly DependencyProperty PropertyNameProperty = DependencyProperty.Register("PropertyName", typeof(string), typeof(EventCommandConverterBehavior), new PropertyMetadata());
private static void OnEventChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs args)
{
var behavior = (EventCommandConverterBehavior)dependencyObject;
if (behavior.AssociatedObject != null)
behavior.AttachHandler((string)args.NewValue);
}
protected override void OnAttached()
{
AttachHandler(this.EventName);
}
private void AttachHandler(string eventName)
{
// detach old event
if (this.oldEvent != null)
this.oldEvent.RemoveEventHandler(this.AssociatedObject, this.eventHandler);
// attach new event
if (string.IsNullOrEmpty(eventName))
{
return;
}
var eventInfo = this.AssociatedObject.GetType().GetEvent(eventName);
if (eventInfo != null)
{
var methodInfo = this.GetType()
.GetMethod("CommandExecuter", BindingFlags.Instance | BindingFlags.NonPublic);
this.eventHandler = Delegate.CreateDelegate(eventInfo.EventHandlerType, this, methodInfo);
eventInfo.AddEventHandler(this.AssociatedObject, this.eventHandler);
this.oldEvent = eventInfo;
}
else
throw new ArgumentException(string.Format("Type {0} does not contain event {1} definition.",
this.AssociatedObject.GetType().Name, eventName));
}
private void CommandExecuter(object sender, EventArgs e)
{
object parameter = null != PropertyName ? GetPropValue(sender, PropertyName) : e;
if (this.Command != null)
{
if (this.Command.CanExecute(parameter))
this.Command.Execute(parameter);
}
}
public static object GetPropValue(object src, string propName)
{
var propertyInfo = src.GetType().GetProperty(propName);
return propertyInfo != null ? propertyInfo.GetValue(src, null) : null;
}
}
In Xaml you have to write
<TextBox Name="TextBox1" Text="SomeText" Height="20" Width="60">
<i:Interaction.Behaviors >
<wpfPractice:EventCommandConverterBehavior Command="{Binding PropertyValueCommand}" EventName="SelectionChanged" PropertyName="CaretIndex" />
</i:Interaction.Behaviors>
</TextBox>
Where PropertyValueCommand shold be a command in DataContext(i.e. ViewModel.
let me know if it works.
in my WPF application I have several forms with a multitude of input fields for the user. As it turns out not every user needs all fields depending on its companie's process therefore I have the new requirement to allow the user to hide fields depending on its own needs.
I was planning to use a Behavior for this which could be attached to basically every WPF-Control. The behavior would add a ContextMenu to each of the controls allowing to show/hide all of the available Fields. My current test project which seems to work nice has four DependencyProperties to make everything work:
string VisibilityGroupName:
This acts somehow as an Id for each field but is not unique in order to group multiple fields togther (eG a Label for a field caption to its corresponding TextBox). This string is currently also used as the name that the user sees in the ContextMenu.
Dictionary VisibilityDictionary:
This dictionary keeps track of all the visibility states of the fields. In my application I'm going to serialize this to XML in order to make the users decisions persistant.
bool AllowCustomVisibility:
This is just a flag in order to switch the whole funtionality off.
bool NotificationDummy:
This is where it gets interesting. Currently I abuse this property in conjunction with an ValueChanged-event to notify all the Controls that a state was changed so they can check if they are affected. While this works as expected I know that this is just a bad workaround because I don't know how the notification would be done correctly.
Does anybody have an idea how it is done correctly? I've marked the corresponding places in the code with TODOs:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interactivity;
namespace CustomizableUserControlVisibility
{
public class WPFCustomVisibilityBehavior : Behavior<DependencyObject>
{
#region Fields
private Control _control = null;
private ContextMenu _contextMenu;
private bool _contextMenuIsBuilt = false;
#endregion
#region Properties
public bool NotificationDummy
{
get { return (bool)GetValue(NotificationDummyProperty); }
set { SetValue(NotificationDummyProperty, value); }
}
public static readonly DependencyProperty NotificationDummyProperty = DependencyProperty.Register("NotificationDummy", typeof(bool), typeof(WPFCustomVisibilityBehavior), new PropertyMetadata(false));
public bool AllowCustomVisibility
{
get { return (bool)GetValue(AllowCustomVisibilityProperty); }
set { SetValue(AllowCustomVisibilityProperty, value); }
}
public static readonly DependencyProperty AllowCustomVisibilityProperty = DependencyProperty.Register("AllowCustomVisibility", typeof(bool), typeof(WPFCustomVisibilityBehavior), new PropertyMetadata(false));
public string VisibilityGroupName
{
get { return (string)GetValue(VisibilityGroupNameProperty); }
set { SetValue(VisibilityGroupNameProperty, value); }
}
public static readonly DependencyProperty VisibilityGroupNameProperty = DependencyProperty.Register("VisibilityGroupName", typeof(string), typeof(WPFCustomVisibilityBehavior), new PropertyMetadata(string.Empty));
public Dictionary<string, bool> VisibilityDictionary
{
get { return (Dictionary<string, bool>)GetValue(VisibilityDictionaryProperty); }
set { SetValue(VisibilityDictionaryProperty, value); }
}
public static readonly DependencyProperty VisibilityDictionaryProperty = DependencyProperty.Register("VisibilityDictionary", typeof(Dictionary<string, bool>), typeof(WPFCustomVisibilityBehavior), new PropertyMetadata(new Dictionary<string, bool>()));
#endregion
#region Constructor
public WPFCustomVisibilityBehavior()
{
// TODO: There should be a better way to notify other controls about state changes than this...
var temp = DependencyPropertyDescriptor.FromProperty(WPFCustomVisibilityBehavior.NotificationDummyProperty, typeof(WPFCustomVisibilityBehavior));
if (temp != null)
{
temp.AddValueChanged(this, OnNotificationDummyChanged);
}
}
#endregion
#region Overrrides
protected override void OnAttached()
{
base.OnAttached();
if (this.AllowCustomVisibility == false)
{
return;
}
this._control = this.AssociatedObject as Control;
if (!string.IsNullOrEmpty(this.VisibilityGroupName) && this._control != null)
{
if (this.VisibilityDictionary.ContainsKey(this.VisibilityGroupName))
{
if (this.VisibilityDictionary[this.VisibilityGroupName])
{
this._control.Visibility = Visibility.Visible;
}
else
{
this._control.Visibility = Visibility.Collapsed;
}
}
else
{
this.VisibilityDictionary.Add(this.VisibilityGroupName, this._control.Visibility == Visibility.Visible ? true : false);
}
// Add a ContextMenu to the Control, but only if it does not already have one (TextBox brings its default ContextMenu for copy, cut and paste)
if (this._control.ContextMenu == null && !(this._control is TextBox))
{
this._contextMenu = new ContextMenu();
ContextMenuService.SetContextMenu(this._control, this._contextMenu);
this._control.ContextMenuOpening += (sender, e) => { ContextMenuOpening(e); };
}
}
}
#endregion
#region Event handling
private void ContextMenuOpening(ContextMenuEventArgs e)
{
if (this._contextMenuIsBuilt == false)
{
this._contextMenu.Items.Clear();
Dictionary<string, MenuItem> menuItems = new Dictionary<string, MenuItem>();
foreach (string k in this.VisibilityDictionary.Keys)
{
MenuItem menuItem = new MenuItem() { Header = k, Name = k, IsCheckable = true, StaysOpenOnClick = true };
menuItem.Click += MenuItem_Click;
menuItems.Add(k, menuItem);
}
var keyList = menuItems.Keys.ToList();
keyList.Sort();
foreach (string key in keyList)
{
this._contextMenu.Items.Add(menuItems[key]);
}
this._contextMenuIsBuilt = true;
}
foreach (MenuItem mi in this._contextMenu.Items)
{
mi.IsChecked = this.VisibilityDictionary[mi.Name];
}
}
private void MenuItem_Click(object sender, RoutedEventArgs e)
{
MenuItem mi = sender as MenuItem;
if (mi != null && this.VisibilityDictionary != null && this.VisibilityDictionary.ContainsKey(mi.Name))
{
this.VisibilityDictionary[mi.Name] = mi.IsChecked;
// TODO: There should be a better way to notify other controls about state changes than this...
this.NotificationDummy = !NotificationDummy;
}
}
private void OnNotificationDummyChanged(object sender, EventArgs args)
{
// TODO: There should be a better way to notify other controls about state changes than this...
if (this._control != null && this.VisibilityDictionary != null && !string.IsNullOrEmpty(this.VisibilityGroupName))
{
if (this.VisibilityDictionary.ContainsKey(this.VisibilityGroupName))
{
if (this.VisibilityDictionary[this.VisibilityGroupName])
{
this._control.Visibility = Visibility.Visible;
}
else
{
this._control.Visibility = Visibility.Collapsed;
}
}
}
}
#endregion
}
}
Due to the lack of any other ideas I decided to use a static Event which seems to solve my problem quite well and this approach at leasts saves me the NotificationDummy-DependencyProperty which I had to use in the first place.
If anybody is interested - here is my final solution:
namespace CustomizableUserControlVisibility
{
public delegate void VisibilityChangedEventHandler(object visibilityDictionary);
public class WPFCustomVisibilityBehavior : Behavior<DependencyObject>
{
#region Fields
public static event VisibilityChangedEventHandler OnVisibilityChanged;
private Control _control = null;
private ContextMenu _contextMenu;
private bool _contextMenuIsBuilt = false;
#endregion
#region Properties
public bool AllowCustomVisibility
{
get { return (bool)GetValue(AllowCustomVisibilityProperty); }
set { SetValue(AllowCustomVisibilityProperty, value); }
}
public static readonly DependencyProperty AllowCustomVisibilityProperty = DependencyProperty.Register("AllowCustomVisibility", typeof(bool), typeof(WPFCustomVisibilityBehavior), new PropertyMetadata(false));
public string VisibilityGroupName
{
get { return (string)GetValue(VisibilityGroupNameProperty); }
set { SetValue(VisibilityGroupNameProperty, value); }
}
public static readonly DependencyProperty VisibilityGroupNameProperty = DependencyProperty.Register("VisibilityGroupName", typeof(string), typeof(WPFCustomVisibilityBehavior), new PropertyMetadata(string.Empty));
public Dictionary<string, bool> VisibilityDictionary
{
get { return (Dictionary<string, bool>)GetValue(VisibilityDictionaryProperty); }
set { SetValue(VisibilityDictionaryProperty, value); }
}
public static readonly DependencyProperty VisibilityDictionaryProperty = DependencyProperty.Register("VisibilityDictionary", typeof(Dictionary<string, bool>), typeof(WPFCustomVisibilityBehavior), new PropertyMetadata(new Dictionary<string, bool>()));
#endregion
#region Constructor
public WPFCustomVisibilityBehavior()
{
OnVisibilityChanged += VisibilityChanged;
}
#endregion
#region Overrrides
protected override void OnAttached()
{
base.OnAttached();
if (this.AllowCustomVisibility == false)
{
return;
}
this._control = this.AssociatedObject as Control;
if (!string.IsNullOrEmpty(this.VisibilityGroupName) && this._control != null)
{
if (this.VisibilityDictionary.ContainsKey(this.VisibilityGroupName))
{
if (this.VisibilityDictionary[this.VisibilityGroupName])
{
this._control.Visibility = Visibility.Visible;
}
else
{
this._control.Visibility = Visibility.Collapsed;
}
}
else
{
this.VisibilityDictionary.Add(this.VisibilityGroupName, this._control.Visibility == Visibility.Visible ? true : false);
}
}
// Add a ContextMenu to the Control, but only if it does not already have one (TextBox brings its default ContextMenu for copy, cut and paste)
if (this._control != null && this._control.ContextMenu == null && !(this._control is TextBox))
{
this._contextMenu = new ContextMenu();
ContextMenuService.SetContextMenu(this._control, this._contextMenu);
this._control.ContextMenuOpening += (sender, e) => { ContextMenuOpening(e); };
}
}
#endregion
#region Event handling
private void ContextMenuOpening(ContextMenuEventArgs e)
{
if (this._contextMenuIsBuilt == false)
{
// Clear Items just to be sure there is nothing in it...
this._contextMenu.Items.Clear();
// Create default items first
MenuItem showAll = new MenuItem() { Header = "Show all optional fields", IsCheckable = false, FontWeight = FontWeights.Bold };
showAll.Click += MenuItem_ShowAll_Click;
MenuItem hideAll = new MenuItem() { Header = "Hide all optional fields", IsCheckable = false, FontWeight = FontWeights.Bold };
hideAll.Click += MenuItem_HideAll_Click;
// Create field items and sort them by name
Dictionary<string, MenuItem> menuItems = new Dictionary<string, MenuItem>();
foreach (string k in this.VisibilityDictionary.Keys)
{
MenuItem menuItem = new MenuItem() { Header = k, Name = k, IsCheckable = true, StaysOpenOnClick = true };
menuItem.Click += MenuItem_Click;
menuItems.Add(k, menuItem);
}
var keyList = menuItems.Keys.ToList();
keyList.Sort();
// Now add default items followed by field items
this._contextMenu.Items.Add(showAll);
this._contextMenu.Items.Add(hideAll);
this._contextMenu.Items.Add(new Separator());
foreach (string key in keyList)
{
this._contextMenu.Items.Add(menuItems[key]);
}
this._contextMenuIsBuilt = true;
}
foreach (Object o in this._contextMenu.Items)
{
MenuItem mi = o as MenuItem;
if (mi != null && mi.FontWeight != FontWeights.Bold)
{
mi.IsChecked = this.VisibilityDictionary[mi.Name];
}
}
}
private void MenuItem_Click(object sender, RoutedEventArgs e)
{
MenuItem mi = sender as MenuItem;
if (mi != null && this.VisibilityDictionary != null && this.VisibilityDictionary.ContainsKey(mi.Name))
{
this.VisibilityDictionary[mi.Name] = mi.IsChecked;
OnVisibilityChanged(this.VisibilityDictionary);
}
}
private void MenuItem_HideAll_Click(object sender, RoutedEventArgs e)
{
List<string> keys = this.VisibilityDictionary.Keys.ToList<string>();
foreach (string key in keys)
{
this.VisibilityDictionary[key] = false;
}
OnVisibilityChanged(this.VisibilityDictionary);
}
private void MenuItem_ShowAll_Click(object sender, RoutedEventArgs e)
{
List<string> keys = this.VisibilityDictionary.Keys.ToList<string>();
foreach (string key in keys)
{
this.VisibilityDictionary[key] = true;
}
OnVisibilityChanged(this.VisibilityDictionary);
}
private void VisibilityChanged(object visibilityDictionary)
{
if (this._control != null && this.VisibilityDictionary != null && this.VisibilityDictionary == visibilityDictionary && !string.IsNullOrEmpty(this.VisibilityGroupName))
{
if (this.VisibilityDictionary.ContainsKey(this.VisibilityGroupName))
{
if (this.VisibilityDictionary[this.VisibilityGroupName])
{
this._control.Visibility = Visibility.Visible;
}
else
{
this._control.Visibility = Visibility.Collapsed;
}
}
}
}
#endregion
}
}
I created a UserControl1 that wraps a DataGrid (this is simplified for test purposes, the real scenario involves a third-party control but the issue is the same). The UserControl1 is used in the MainWindow of the test app like so:
<test:UserControl1 ItemsSource="{Binding People,Mode=OneWay,ElementName=Self}"
SelectedItems="{Binding SelectedPeople, Mode=TwoWay, ElementName=Self}"/>
Everything works as expected except that when a row is selected in the DataGrid, the SelectedPeople property is always set to null.
The row selection flow is roughly: UserControl1.DataGrid -> UserControl1.DataGrid_OnSelectionChanged -> UserControl1.SelectedItems -> MainWindow.SelectedPeople
Debugging shows the IList with the selected item from the DataGrid is being passed to the SetValue call of the SelectedItems dependency property. But when the SelectedPeople setter is subsequently called (as part of the binding process) the value passed to it is always null.
Here's the relevant UserControl1 XAML:
<Grid>
<DataGrid x:Name="dataGrid" SelectionChanged="DataGrid_OnSelectionChanged" />
</Grid>
In the code-behind of UserControl1 are the following definitions for the SelectedItems dependency properties and the DataGrid SelectionChanged handler:
public static readonly DependencyProperty SelectedItemsProperty = DependencyProperty.Register("SelectedItems", typeof(IList), typeof(UserControl1), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSelectedItemsChanged));
public IList SelectedItems
{
get { return (IList)GetValue(SelectedItemsProperty); }
set
{
SetValue(SelectedItemsProperty, value);
}
}
private bool _isUpdatingSelectedItems;
private static void OnSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var ctrl = d as UserControl1;
if ((ctrl != null) && !ctrl._isUpdatingSelectedItems)
{
ctrl._isUpdatingSelectedItems = true;
try
{
ctrl.dataGrid.SelectedItems.Clear();
var selectedItems = e.NewValue as IList;
if (selectedItems != null)
{
var validSelectedItems = selectedItems.Cast<object>().Where(item => ctrl.ItemsSource.Contains(item) && !ctrl.dataGrid.SelectedItems.Contains(item)).ToList();
validSelectedItems.ForEach(item => ctrl.dataGrid.SelectedItems.Add(item));
}
}
finally
{
ctrl._isUpdatingSelectedItems = false;
}
}
}
private void DataGrid_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (!_isUpdatingSelectedItems && sender is DataGrid)
{
_isUpdatingSelectedItems = true;
try
{
var x = dataGrid.SelectedItems;
SelectedItems = new List<object>(x.Cast<object>());
}
finally
{
_isUpdatingSelectedItems = false;
}
}
}
Here is definition of SomePeople from MainWindow code-behind:
private ObservableCollection<Person> _selectedPeople;
public ObservableCollection<Person> SelectedPeople
{
get { return _selectedPeople; }
set { SetProperty(ref _selectedPeople, value); }
}
public class Person
{
public Person(string first, string last)
{
First = first;
Last = last;
}
public string First { get; set; }
public string Last { get; set; }
}
I faced the same problem, i dont know reason, but i resolved it like this:
1) DP
public static readonly DependencyProperty SelectedItemsProperty = DependencyProperty.Register("SelectedItems", typeof(object), typeof(UserControl1),
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSelectedItemsChanged));
public object SelectedItems
{
get { return (object) GetValue(SelectedItemsProperty); }
set { SetValue(SelectedItemsProperty, value); }
}
2) Grid event
private void DataGrid_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
var SelectedItemsCasted = SelectedItems as IList<object>;
if (SelectedItemsCasted == null)
return;
foreach (object addedItem in e.AddedItems)
{
SelectedItemsCasted.Add(addedItem);
}
foreach (object removedItem in e.RemovedItems)
{
SelectedItemsCasted.Remove(removedItem);
}
}
3) In UC which contain UserControl1
Property:
public IList<object> SelectedPeople { get; set; }
Constructor:
public MainViewModel()
{
SelectedPeople = new List<object>();
}
I know this is a super old post- but after digging through this, and a few other posts which address this issue, I couldn't find a complete working solution. So with the concept from this post I am doing that.
I've also created a GitHub repo with the complete demo project which contains more comments and explanation of the logic than this post. MultiSelectDemo
I was able to create an AttachedProperty (with some AttachedBehavour logic as well to set up the SelectionChanged handler).
MultipleSelectedItemsBehaviour
public class MultipleSelectedItemsBehaviour
{
public static readonly DependencyProperty MultipleSelectedItemsProperty =
DependencyProperty.RegisterAttached("MultipleSelectedItems", typeof(IList), typeof(MultipleSelectedItemsBehaviour),
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, MultipleSelectedItemsChangedCallback));
public static IList GetMultipleSelectedItems(DependencyObject d) => (IList)d.GetValue(MultipleSelectedItemsProperty);
public static void SetMultipleSelectedItems(DependencyObject d, IList value) => d.SetValue(MultipleSelectedItemsProperty, value);
public static void MultipleSelectedItemsChangedCallback(object sender, DependencyPropertyChangedEventArgs e)
{
if (sender is DataGrid dataGrid)
{
if (e.NewValue == null)
{
dataGrid.SelectionChanged -= DataGrid_SelectionChanged;
}
else
{
dataGrid.SelectionChanged += DataGrid_SelectionChanged;
}
}
}
private static void DataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (sender is DataGrid dataGrid)
{
var selectedItems = GetMultipleSelectedItems(dataGrid);
if (selectedItems == null) return;
foreach (var item in e.AddedItems)
{
try
{
selectedItems.Add(item);
}
catch (ArgumentException)
{
}
}
foreach (var item in e.RemovedItems)
{
selectedItems.Remove(item);
}
}
}
}
To use it, one critical thing within the view model, is that the view model collection must be initialized so that the attached property/behaviour sets up the SelectionChanged handler. In this example I've done that in the VM constructor.
public MainWindowViewModel()
{
MySelectedItems = new ObservableCollection<MyItem>();
}
private ObservableCollection<MyItem> _myItems;
public ObservableCollection<MyItem> MyItems
{
get => _myItems;
set => Set(ref _myItems, value);
}
private ObservableCollection<MyItem> _mySelectedItems;
public ObservableCollection<MyItem> MySelectedItems
{
get => _mySelectedItems;
set
{
// Remove existing handler if there is already an assignment made (aka the property is not null).
if (MySelectedItems != null)
{
MySelectedItems.CollectionChanged -= MySelectedItems_CollectionChanged;
}
Set(ref _mySelectedItems, value);
// Assign the collection changed handler if you need to know when items were added/removed from the collection.
if (MySelectedItems != null)
{
MySelectedItems.CollectionChanged += MySelectedItems_CollectionChanged;
}
}
}
private int _selectionCount;
public int SelectionCount
{
get => _selectionCount;
set => Set(ref _selectionCount, value);
}
private void MySelectedItems_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
// Do whatever you want once the items are added or removed.
SelectionCount = MySelectedItems != null ? MySelectedItems.Count : 0;
}
And finally to use it in the XAML
<DataGrid Grid.Row="0"
ItemsSource="{Binding MyItems}"
local:MultipleSelectedItemsBehaviour.MultipleSelectedItems="{Binding MySelectedItems}" >
</DataGrid>
I have a strange situation.
I working on an App for WINRT and have some problems with a command binding. This is the part of the xaml:
<control:ItemsHub ItemsSource="{Binding Categories}">
<control:ItemsHub.SectionHeaderTemplate>
<DataTemplate>
<Button Command="{Binding CategoryNavigationCommand}" Margin="5,0,0,10" Content="{Binding Header}"/>
</DataTemplate>
</control:ItemsHub.SectionHeaderTemplate>
</control:ItemsHub>
This is my ViewModel:
public CategorySectionViewModel(IRecipeService recipeService, INavigationService navigationService, RecipeTreeItemDto treeItem)
{
...
CategoryNavigationCommand = new DelegateCommand(NavigateToCategory);
...
}
private string _header;
public string Header
{
get { return _header; }
set { SetProperty(ref _header, value); }
}
public DelegateCommand CategoryNavigationCommand { get; private set; }
private void NavigateToCategory()
{
_navigationService.Navigate("CategoryHub", _recipeTreeItemDto.NodePath);
}
I don't get any binding errors in the output window and also the "Header" gets shown in the Button. But the Command won't get fired wenn i click on it! What i'm doing wrong?
Maybe it is because i created a custom HubControl. With this control i'm able to attach an ItemSource and ItemTemplate for the HubSection-Header and the HubSection-Content. Maybe because of this some bindings getting lost?
Here is my custom hub control:
public class ItemsHub : Hub
{
public DataTemplate ItemTemplate
{
get { return (DataTemplate)GetValue(ItemTemplateProperty); }
set { SetValue(ItemTemplateProperty, value); }
}
public IList ItemsSource
{
get { return (IList)GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
public HubSection SpotlightSection
{
get { return (HubSection)GetValue(SpotlightSectionProperty); }
set { SetValue(SpotlightSectionProperty, value); }
}
public DataTemplate SectionHeaderTemplate
{
get { return (DataTemplate)GetValue(SectionHeaderTemplateProperty); }
set { SetValue(SectionHeaderTemplateProperty, value); }
}
public static readonly DependencyProperty ItemTemplateProperty =
DependencyProperty.Register("ItemTemplate", typeof(DataTemplate), typeof(ItemsHub), new PropertyMetadata(null, ItemTemplateChanged));
public static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register("ItemsSource", typeof(IList), typeof(ItemsHub), new PropertyMetadata(null, ItemsSourceChanged));
public static readonly DependencyProperty SpotlightSectionProperty =
DependencyProperty.Register("SpotlightSection", typeof(HubSection), typeof(ItemsHub), new PropertyMetadata(default(HubSection), SpotlightSectionChanged));
public static readonly DependencyProperty SectionHeaderTemplateProperty =
DependencyProperty.Register("SectionHeaderTemplate", typeof(DataTemplate), typeof(ItemsHub), new PropertyMetadata(default(DataTemplate), HeaderTemplateChanged));
private static void SpotlightSectionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ItemsHub hub = d as ItemsHub;
if (hub != null)
{
bool hubContainsSpotLight = hub.Sections.Contains(hub.SpotlightSection);
if (hub.SpotlightSection != null && !hubContainsSpotLight)
{
hub.Sections.Add(hub.SpotlightSection);
}
if (hub.SpotlightSection == null && hubContainsSpotLight)
{
hub.Sections.Remove(hub.SpotlightSection);
}
}
}
private static void HeaderTemplateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ItemsHub hub = d as ItemsHub;
if (hub != null)
{
DataTemplate template = e.NewValue as DataTemplate;
if (template != null)
{
// Apply template
foreach (var section in hub.Sections.Except(new List<HubSection> { hub.SpotlightSection }))
{
section.HeaderTemplate = template;
}
}
}
}
private static void ItemTemplateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ItemsHub hub = d as ItemsHub;
if (hub != null)
{
DataTemplate template = e.NewValue as DataTemplate;
if (template != null)
{
// Apply template
foreach (var section in hub.Sections.Except(new List<HubSection> { hub.SpotlightSection }))
{
section.ContentTemplate = template;
}
}
}
}
private static void ItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ItemsHub hub = d as ItemsHub;
if (hub != null)
{
IList items = e.NewValue as IList;
if (items != null)
{
var spotLightSection = hub.SpotlightSection;
hub.Sections.Clear();
if (spotLightSection != null)
{
hub.Sections.Add(spotLightSection);
}
foreach (var item in items)
{
HubSection section = new HubSection();
section.DataContext = item;
section.Header = item;
DataTemplate headerTemplate = hub.SectionHeaderTemplate;
section.HeaderTemplate = headerTemplate;
DataTemplate contentTemplate = hub.ItemTemplate;
section.ContentTemplate = contentTemplate;
hub.Sections.Add(section);
}
}
}
}
}
Thanks for your help!
I found the solution! I don't know why but the section header of a hub control is not interactive by default! So i have to set the interactivity to true when i assign the itemsource in my custom ItemHub-Control.
Here is what i changed in the ItemsHub Control:
private static void ItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ItemsHub hub = d as ItemsHub;
if (hub != null)
{
IList items = e.NewValue as IList;
if (items != null)
{
var spotLightSection = hub.SpotlightSection;
hub.Sections.Clear();
if (spotLightSection != null)
{
hub.Sections.Add(spotLightSection);
}
foreach (var item in items)
{
HubSection section = new HubSection();
DataTemplate headerTemplate = hub.SectionHeaderTemplate;
section.HeaderTemplate = headerTemplate;
DataTemplate contentTemplate = hub.ItemTemplate;
section.ContentTemplate = contentTemplate;
section.DataContext = item;
section.Header = item;
//This line fixed my problem.
section.IsHeaderInteractive = true;
hub.Sections.Add(section);
}
}
}
You can use following binding expression
Command="{Binding CategoryNavigationCommand,RelativeSource={RelativeSource AncestorType=XYX}}"
and XYZ is a control type in which you have placed ItemsHub
I have a ListBox with binding to a list of strings. I want to filter the list when I enter text in a TextBox. How can I do it?
public void ListLoad()
{
ElementList = new List<string>(); // creation a list of strings
ElementList.Add("1"); // add a item of string
ElementList.Add("2"); // add a item of string
DataContext = this; // set the data context
}
I'm binding it in XAML with:
ItemsSource="{Binding ElementList}"
CollectionViewSource class can help here. As far as I can tell it has many capabilities to filter, sort and group collections.
ICollectionView view = CollectionViewSource.GetDefaultView(ElementList);
view.Filter = (o) => {return o;}//here is the lambda with your conditions to filter
When you don't need any filter just set view.Filter to null.
Also check out this article on filtering
Here is an attached property for binding a filter:
using System;
using System.Windows;
using System.Windows.Controls;
public static class Filter
{
public static readonly DependencyProperty ByProperty = DependencyProperty.RegisterAttached(
"By",
typeof(Predicate<object>),
typeof(Filter),
new PropertyMetadata(default(Predicate<object>), OnByChanged));
public static void SetBy(ItemsControl element, Predicate<object> value)
{
element.SetValue(ByProperty, value);
}
public static Predicate<object> GetBy(ItemsControl element)
{
return (Predicate<object>)element.GetValue(ByProperty);
}
private static void OnByChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is ItemsControl itemsControl &&
itemsControl.Items.CanFilter)
{
itemsControl.Items.Filter = (Predicate<object>)e.NewValue;
}
}
}
Used like this in xaml:
<DataGrid local:Filter.By="{Binding Filter}"
ItemsSource="{Binding Foos}">
...
And viewmodel:
public class ViewModel : INotifyPropertyChanged
{
private string filterText;
private Predicate<object> filter;
public event PropertyChangedEventHandler PropertyChanged;
public ObservableCollection<Foo> Foos { get; } = new ObservableCollection<Foo>();
public string FilterText
{
get { return this.filterText; }
set
{
if (value == this.filterText) return;
this.filterText = value;
this.OnPropertyChanged();
this.Filter = string.IsNullOrEmpty(this.filterText) ? (Predicate<object>)null : this.IsMatch;
}
}
public Predicate<object> Filter
{
get { return this.filter; }
private set
{
this.filter = value;
this.OnPropertyChanged();
}
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private bool IsMatch(object item)
{
return IsMatch((Foo)item, this.filterText);
}
private static bool IsMatch(Foo item, string filterText)
{
if (string.IsNullOrEmpty(filterText))
{
return true;
}
var name = item.Name;
if (string.IsNullOrEmpty(name))
{
return false;
}
if (filterText.Length == 1)
{
return name.StartsWith(filterText, StringComparison.OrdinalIgnoreCase);
}
return name.IndexOf(filterText, 0, StringComparison.OrdinalIgnoreCase) >= 0;
}
}
If you set Dictionary as itemsource to listbox use the below code to sort,
private void tb_filter_textChanged(object sender, TextChangedEventArgs e)
{
Dictionary<string, string> dictObject = new Dictionary<string, string>();
ICollectionView view = CollectionViewSource.GetDefaultView(dictObject);
view.Filter = CustomerFilter;
listboxname.ItemsSource = view;
}
private bool CustomerFilter(object item)
{
KeyValuePair<string, string> Items = (KeyValuePair<string,string>) item;
return Items.Value.ToString().Contains("a");
}
The above code returns the items contaning "a".