I have a view model that presents items in ListBox. There are two collection: Source contains all element and Checked contains only checked elements. There are two buttons SelectAll and ClearAll. When I push on one of this button View Model works well and both Source and Checked collections updating, but no changes in Listbox happens.
CheckItemPresenterVM<T> - an element that save the state of one button and implements INotifyPropertyChange, but when property IsChecked changes CollectionChangedevent doesn't reise.
The question is how to make it work?
<UserControl.Resources>
<ItemsPanelTemplate x:Key="listPanelTemplate">
<WrapPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
<Style TargetType="ListBox">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<ScrollViewer HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Hidden">
<ItemsPresenter/>
</ScrollViewer>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="ListBoxItemIsCheckedBinding"
TargetType="{x:Type ListBoxItem}">
<Setter Property="IsSelected"
Value="{Binding IsChecked, Mode=TwoWay}" />
<Setter Property="Padding"
Value="5,3" />
<Setter Property="Background"
Value="LightCyan"></Setter>
</Style>
<Style TargetType="Button">
<Setter Property="Padding"
Value="7, 3" />
<Setter Property="Margin"
Value="1" />
</Style>
<Style TargetType="ToggleButton">
<Setter Property="Padding"
Value="7, 3" />
<Setter Property="Margin"
Value="1" />
</Style>
</UserControl.Resources>
<Grid>
<Expander Header="Categories"
IsExpanded="True">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<WrapPanel Grid.Row="0"
Margin="0, 5">
<Button Command="{Binding SelectAll}">Select All</Button>
<Button Command="{Binding ClearAll}">Clear All</Button>
<Button Command="{Binding InvertSelection}">Invert Selection</Button>
</WrapPanel>
<ListBox Grid.Row="1" />
<ListBox Name="CategoriesListBox"
Grid.Row="2"
SelectionMode="Multiple"
ItemsSource="{Binding CategoryVM.Source, Mode=TwoWay}"
ItemContainerStyle="{DynamicResource ListBoxItemIsCheckedBinding}"
ItemsPanel="{StaticResource listPanelTemplate}" />
<ListBox Name="CheckedListBox"
Grid.Row="3"
SelectionMode="Multiple"
ItemsSource="{Binding CategoryVM.Checked}"/>
</Grid>
</Expander>
</Grid>
class TestViewModel
{
public CheckItemVM<string> CategoryVM { get; set; }
public TestViewModel()
{
logger.FileName = "TestSelectionFilter.txt";
logger.AddEventRecord(this);
CategoryVM = new CheckItemVM<string>(
new List<string>() {
"01 elem",
"02 elem",
"03 elem",
"04 elem",
"05 elem",
"06 elem",
"07 elem",
"08 elem",
"09 elem",
"10 elem",
"11 elem",
"12 elem",
"13 elem",
"14 elem",
"15 elem",
"16 elem",
"17 elem",
"18 elem",
"19 elem",
"20 elem",
"21 elem",
"22 elem",
"23 elem",
"24 elem",
"25 elem"});
}
public ICommand SelectAll
{
get
{
return new RelayCommand
{
ExecuteAction = a =>
{
CategoryVM.SelectAll();
},
CanExecutePredicate = p =>
{
return !CategoryVM.IsAllSelected();
}
};
}
}
public ICommand InvertSelection
{
get
{
return new RelayCommand
{
ExecuteAction = a =>
{
CategoryVM.InvertSelection();
},
CanExecutePredicate = p =>
{
return true;
}
};
}
}
public ICommand ClearAll
{
get
{
return new RelayCommand
{
ExecuteAction = a =>
{
CategoryVM.ClearAll();
},
CanExecutePredicate = p =>
{
return CategoryVM.Checked.Any();
}
};
}
}
class CheckItemVM<T>
{
protected ObservableCollection<CheckItemPresenterVM<T>> _checked = new ObservableCollection<CheckItemPresenterVM<T>>();
public ObservableCollection<CheckItemPresenterVM<T>> Source { get; set; }
public ObservableCollection<CheckItemPresenterVM<T>> Checked { get => _checked; }
public CheckItemVM(ICollection<T> _source)
{
Source = new ObservableCollection<CheckItemPresenterVM<T>>();
UpdateSource(_source);
}
protected void UpdateSource(ICollection<T> _source)
{
foreach (var item in _source)
{
Source.Add(new CheckItemPresenterVM<T>(ref _checked)
{ Item = item, IsChecked = false });
}
}
public void SetSelection(CheckItemPresenterVM<T> sourceItem, bool flag)
{
if (sourceItem.IsChecked != flag)
{
sourceItem.IsChecked = flag;
}
}
public void SelectAll()
{
foreach (var item in Source)
{
item.IsChecked = true;
}
}
public void ClearAll()
{
foreach (var item in Source)
{
item.IsChecked = false;
}
}
public void InvertSelection()
{
foreach (var item in Source)
{
if (item.IsChecked) item.IsChecked = false;
else item.IsChecked = true;
}
}
public bool IsAllSelected()
{
return Source.Count == Checked.Count;
}
}
class CheckItemPresenterVM<T> : INotifyPropertyChanged
{
protected bool _isChecked;
protected ObservableCollection<CheckItemPresenterVM<T>> _checked;
public CheckItemPresenterVM(ref ObservableCollection<CheckItemPresenterVM<T>> Checked)
{
_checked = Checked;
}
public T Item { get; set; }
public string Name { get; set; }
public bool IsChecked
{
get
{
return _isChecked;
}
set
{
_isChecked = value;
if (value)
{
if (!_checked.Contains(this))
{
_checked.Add(this);
NotifyPropertyChanged("Item was Checked");
}
}
else
{
if (_checked.Contains(this))
{
_checked.Remove(this);
NotifyPropertyChanged("Item Unchecked");
}
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public override string ToString()
{
return Item.ToString();
}
}
I see strange part of you code.
Guessing this fix in PropertyChanged raising call
public bool IsChecked
{
get
{
return _isChecked;
}
set
{
_isChecked = value;
if (value)
{
if (!_checked.Contains(this))
{
_checked.Add(this);
NotifyPropertyChanged("IsChecked");
}
}
else
{
if (_checked.Contains(this))
{
_checked.Remove(this);
NotifyPropertyChanged("IsChecked");
}
}
}
}
NotifyPropertyChanged fires PoropertyChanged Event with a string parameter you passing to. Meanwhile Binding will receive it.
So, here's 2 friends
<Setter Property="IsSelected" Value="{Binding IsChecked, Mode=TwoWay}" />
and
NotifyPropertyChanged("IsChecked");
If you need tune the update behavior of your control, pass UpdateSourceTrigger setting to to the binding, this way:
<Setter Property="IsSelected" Value="{Binding IsChecked, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
Additionally: TwoWay is default for the Mode here. You may not declare it.
When binding a View/Control to an ObservableCollection the View/Control will be redrawn when the CollectionChanged event of ObservableCollection is raised.
This occurs when an item is added, removed, reordered or assigned a new instance; but not (as you've probably realised) when an item raises it's PropertyChanged event.
To get your changes to be reflected in the UI you need the PropertyChanged of each Item to raise the CollectionChanged event of the collection.
public class ObservableItemCollection<T> : ObservableCollection<T> where T : INotifyPropertyChanged
{
// Call this from the constructor
private void InitialiseItems()
{
CollectionChanged += ContentCollectionChanged;
foreach (T item in Items)
item.PropertyChanged += ReplaceElementWithItself;
}
private void ContentCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.OldItems != null)
{
foreach (T item in e.OldItems)
{
item.PropertyChanged -= ReplaceElementWithItself;
}
}
if (e.NewItems != null)
{
foreach (T item in e.NewItems)
{
item.PropertyChanged += ReplaceElementWithItself;
}
}
}
private void ReplaceElementWithItself(object sender, PropertyChangedEventArgs e)
{
var collectionChangedArgs = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, sender, sender, IndexOf((T)sender));
// Call this on the main thread
OnCollectionChanged(collectionChangedArgs);
}
}
This implementation simply raises the CollectionChanged event as a 'Replace' where the same Item is both removed and inserted, at the same index. Beware that it could have unintended consequences if you have anything that actually cares about the CollectionChanged events beyond binding it to the UI; but it is the simplest way I have found to achieve what you are after.
Related
I created a WPF application where I create a list of items to be executed, as a Treeview. On a click event, I parse the ObservableCollection items one by one. This observableCollection is set as the DataContext for the treeview. When running the tests, I want to highlight the current running item in the Treeview.
I have the implemented following code, but the highlighting on the Treeview (visuallY) does not seem to happen. I checked that the "IsSelected" property does get set/unset as programmed.
I am not sure were I went wrong. Could you point out where the mistake is.
I have this class used as a DataContext to the TreeView (named mainTree).
class mytreefile : INotifyPropertyChanged
{
private string _name { get; set; }
public ObservableCollection <mytreefile> children { get; set; }
bool? _isSelected = false;
public bool? IsSelected
{
get { return _isSelected; }
set { SetIsSelected(value); }
}
void SetIsSelected(bool? val)
{
_isSelected = val;
}
public mytreefile(string value)
{
_name = value;
children = new ObservableCollection<mytreefile>();
}
void NotifyPropertyChanged(string info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
The XAML file is
<Grid.Resources>
<ResourceDictionary>
<HierarchicalDataTemplate x:Key="tvTemplate" ItemsSource="{Binding children, Mode=TwoWay}">
<StackPanel Orientation="Horizontal">
<ContentPresenter Content="{Binding _name, Mode=TwoWay}" Margin="2,0" />
</StackPanel>
</HierarchicalDataTemplate>
</ResourceDictionary>
</Grid.Resources>
<TreeView x:Name="mainTree" Grid.Row="0" Grid.Column="0" Grid.RowSpan="4" Background="WhiteSmoke"
Height="Auto" Width="Auto" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
Margin="1,0,2,0" SelectedItemChanged="mainTree_SelectedItemChanged"
ItemTemplate="{StaticResource tvTemplate}"
ItemsSource="{Binding}" DataContext="{Binding nodes}">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=TwoWay}" />
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="FontWeight" Value="Bold" />
</Trigger>
<Trigger Property="IsSelected" Value="False">
<Setter Property="FontWeight" Value="Normal" />
</Trigger>
</Style.Triggers>
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
And my MainWindow code is:
public partial class MainWindow : Window
{
ObservableCollection<mytreefile> nodes = new ObservableCollection<mytreefile>();
mytreefile mtf = null;
Thread thThread = null;
int gnCount = 0;
private void LoadTree ()
{
mytreefile tf1 = new mytreefile("Group1");
nodes.Add(tf1);
mytreefile subtf1 = new mytreefile("Sub Item 1");
mytreefile subtf2 = new mytreefile("Sub Item 2");
mytreefile subtf3 = new mytreefile("Sub Item 3");
mytreefile subtf4 = new mytreefile("Sub Item 4");
tf1.children.Add(subtf1); tf1.children.Add(subtf2); tf1.children.Add(subtf3); tf1.children.Add(subtf4);
maintree.DataContext = nodes;
}
private void OnButton1_click()
{
mtf = nodes.ElementAt(0);
gnCount = 0;
thThread = new Thread(new ThreadStart(this.myThread));
thThread.Start();
}
public void myThread ()
{
for (int i = 0; i < 3; i++)
{
Thread.Sleep(1000);
this.Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.Send,
new Action(() => SetTreeItem(i)));
}
}
public void SetTreeItem(int i)
{
if (gnCount > 0) {
mytreefile mtreeitem = mtf.children.ElementAt(gnCount-1);
mtreeitem.IsSelected = false;
}
mytreefile mtreeitem = mtf.children.ElementAt(gnCount++);
mtreeitem.IsSelected = true;
}
}
The problem was with the "mytreefile" class.
The below class works fine. The way the "IsSelected" implementation was done made the difference. Posting the code for reference.
class mytreefile : INotifyPropertyChanged
{
private string _name { get; set; }
public ObservableCollection <mytreefile> children { get; set; }
private bool _isSelected;
public bool IsSelected
{
get { return _isSelected; }
set
{
if (value != this._isSelected)
{
this._isSelected = value;
NotifyPropertyChanged("IsSelected");
}
}
}
public mytreefile(string value)
{
_name = value;
children = new ObservableCollection<mytreefile>();
}
void NotifyPropertyChanged(string info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
the problem I have is following: when first two rows in DataGrid are selected and the first row is deleted, the selected row below becomes the first row and gets deselected. If I sort any of the columns, the selection of that row comes back. Or, if I close the window I get information that deselected row is actually selected (querying content of underlying bound property SelectedUsers in UsersViewModel - done in OnClosing method). Is there anyone who can help or explain me if I did something wrong or this might be a bug. I provided complete source code below. Thanks for help.
MainWindow.xaml
<Window x:Class="DeleteFirstRowIssue.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:DeleteFirstRowIssue"
Title="MainWindow" Height="350" Width="400">
<Window.Resources>
<Style x:Key="CustomDataGridCellStyle" TargetType="{x:Type DataGridCell}">
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="Red"/>
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="Foreground" Value="Black"/>
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="25"/>
<RowDefinition Height="*"/>
<RowDefinition Height="30"/>
<RowDefinition Height="30"/>
<RowDefinition Height="30"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="Users:" Grid.Row="0" Grid.Column="0"/>
<local:CustomDataGrid x:Name="UsersDataGrid" ItemsSource="{Binding UsersViewSource.View}" SelectionMode="Extended" AlternatingRowBackground="LightBlue" AlternationCount="2"
SelectionUnit="FullRow" IsReadOnly="True" SnapsToDevicePixels="True" AutoGenerateColumns="False" Grid.Row="1" Grid.Column="0" CanUserAddRows="False" CanUserDeleteRows="False" CanUserReorderColumns="False"
SelectedItemsList="{Binding SelectedUsers, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" IsSynchronizedWithCurrentItem="False" CellStyle="{StaticResource CustomDataGridCellStyle}">
<local:CustomDataGrid.Columns>
<DataGridTextColumn Header="Nickname:" Width="*" Binding="{Binding Nickname}"/>
<DataGridTextColumn Header="Age:" Width="*" Binding="{Binding Age}"/>
</local:CustomDataGrid.Columns>
</local:CustomDataGrid>
<Button Grid.Row="2" Grid.Column="0" Margin="5" MaxWidth="80" MinWidth="80" Content="Delete 1st row" Command="{Binding DeleteFirstUserCommand}"/>
<Button Grid.Row="3" Grid.Column="0" Margin="5" MaxWidth="80" MinWidth="80" Content="Delete last row" Command="{Binding DeleteLastUserCommand}"/>
<Button Grid.Row="4" Grid.Column="0" Margin="5" MaxWidth="80" MinWidth="80" Content="Initialize Grid" Command="{Binding InitializeListCommand}"/>
</Grid>
</Window>
MainWindow.xaml.cs
using System;
using System.Collections;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
namespace DeleteFirstRowIssue
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new UsersViewModel();
}
protected override void OnClosing(CancelEventArgs e)
{
UsersViewModel uvm = (UsersViewModel)DataContext;
if (uvm.SelectedUsers.Count > 0)
{
StringBuilder sb = new StringBuilder(uvm.SelectedUsers.Count.ToString() + " selected user(s):\n");
foreach (UserModel um in uvm.SelectedUsers)
{
sb.Append(um.Nickname + "\n");
}
MessageBox.Show(sb.ToString());
}
base.OnClosing(e);
}
}
public class UsersViewModel : INotifyPropertyChanged
{
private IList selectedUsers;
public IList SelectedUsers
{
get { return selectedUsers; }
set
{
selectedUsers = value;
OnPropertyChanged("SelectedUsers");
}
}
public CollectionViewSource UsersViewSource { get; private set; }
public ObservableCollection<UserModel> Users { get; set; }
public ICommand DeleteFirstUserCommand { get; }
public ICommand DeleteLastUserCommand { get; }
public ICommand InitializeListCommand { get; }
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); }
public UsersViewModel()
{
SelectedUsers = new ArrayList();
Users = new ObservableCollection<UserModel>();
UsersViewSource = new CollectionViewSource() { Source = Users };
InitializeListCommand = new RelayCommand(p => Users.Count == 0, p => InitializeList());
InitializeListCommand.Execute(null);
DeleteFirstUserCommand = new RelayCommand(p => Users.Count > 0, p => DeleteFirstUser());
DeleteLastUserCommand = new RelayCommand(p => Users.Count > 0, p => DeleteLastUser());
}
private void InitializeList()
{
Users.Add(new UserModel() { Nickname = "John", Age = 35 });
Users.Add(new UserModel() { Nickname = "Jane", Age = 29 });
Users.Add(new UserModel() { Nickname = "Mark", Age = 59 });
Users.Add(new UserModel() { Nickname = "Susan", Age = 79 });
Users.Add(new UserModel() { Nickname = "Joe", Age = 66 });
Users.Add(new UserModel() { Nickname = "Nina", Age = 29 });
Users.Add(new UserModel() { Nickname = "Selma", Age = 44 });
Users.Add(new UserModel() { Nickname = "Katrin", Age = 24 });
Users.Add(new UserModel() { Nickname = "Joel", Age = 32 });
}
private void DeleteFirstUser()
{
ListCollectionView lcw = (ListCollectionView)UsersViewSource.View;
lcw.RemoveAt(0);
}
private void DeleteLastUser()
{
ListCollectionView lcw = (ListCollectionView)UsersViewSource.View;
lcw.RemoveAt(lcw.Count - 1);
}
}
public class UserModel
{
public string Nickname { get; set; }
public int Age { get; set; }
}
public class CustomDataGrid : DataGrid
{
public static readonly DependencyProperty SelectedItemsListProperty = DependencyProperty.Register("SelectedItemsList", typeof(IList), typeof(CustomDataGrid), new PropertyMetadata(null));
public IList SelectedItemsList
{
get { return (IList)GetValue(SelectedItemsListProperty); }
set { SetValue(SelectedItemsListProperty, value); }
}
public CustomDataGrid() { SelectionChanged += CustomDataGrid_SelectionChanged; }
void CustomDataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e) { SelectedItemsList = SelectedItems; }
}
public class RelayCommand : ICommand
{
private Predicate<object> canExecute;
private Action<object> execute;
public RelayCommand(Predicate<object> canExecute, Action<object> execute)
{
this.canExecute = canExecute;
this.execute = execute;
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public bool CanExecute(object parameter) { return canExecute(parameter); }
public void Execute(object parameter) { execute(parameter); }
}
}
I encountered a similar issue implementing SelectedItemsList. The main gotcha is that when the selection gets renewed, the backing list tries to delete the items that are no longer in the selection. If, however, an item is changed (or no longer exists) the comparison fails and the item remains in the list, causing this behaviour.
You can keep the list in sync by adding a CollectionChanged EventHandler.
public UsersViewModel()
{
...
Users.CollectionChanged += new NotifyCollectionChangedEventHandler(CollectionChanged);
}
private void CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Remove)
foreach (UserModel item in e.OldItems) SelectedUsers.Remove(item);
}
As commented, somehow when the first row is deleted, the new first rows IsSelected property becomes out of sync with its cells IsSelected property. I don't know why this happens, but it is indicating a workaround if you are mainly interested in keeping the style working: Just use the rows property in the trigger
<DataTrigger Binding="{Binding IsSelected,RelativeSource={RelativeSource AncestorType={x:Type DataGridRow}}}" Value="True">
<Setter Property="Background" Value="Red"/>
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="Foreground" Value="Black"/>
</DataTrigger>
I have a data grid with Static Data and two of the items are disabled. The only issue is with Silverlight when using the arrow keys to navigate thru the data grid items the disabled items also get focused on.
I have created a custom data grid class and referenced it in the XAML, and then when using the OnKeyDown event I check for IsEnabled True or False. but so far it is not getting the correct values and I think it is because Where I set the IsEnabled status I am referencing the default Datagrid class?
CustomDataGrid
public class CustomGrid : DataGrid
{
protected override void OnKeyDown(KeyEventArgs e)
{
if(IsEnabled != false)
base.OnKeyDown(e);
}
}
Xaml
<UserControl.Resources>
<Style x:Key="DataGridStyle1" TargetType="local:CustomGrid">
<Setter Property="RowStyle">
<Setter.Value>
<Style TargetType="sdk:DataGridRow">
<Setter Property="IsEnabled" Value="{Binding enabled}"/> //CheckIsEnabled Value
</Style>
</Setter.Value>
</Setter>...
<local:CustomGrid x:Name="McDataGrid" HorizontalAlignment="Left" Height="500" VerticalAlignment="Top" Width="400" Style="{StaticResource DataGridStyle1}"/>
List Data
private List<Model> Data()
{
list.Add(new Model(1, "Test", "1", true));
list.Add(new Model(2, "Ger", "2", true));
list.Add(new Model(3, "dsg", "3", true));
list.Add(new Model(4, "Hd", "4", false));
list.Add(new Model(5, "TeHRFdgst", "5", false));
return list;
}
public class Model : INotifyPropertyChanged
{
public bool _enabled;
public int Id { get; set; }
public string Name { get; set; }
public string Prop { get; set; }
public bool enabled
{
get { return _enabled; }
set
{
_enabled = value;
OnPropertyChanged("enabled");
}
}
public event PropertyChangedEventHandler PropertyChanged;
public Model(int id, string name, string prop, bool isenabled)
{
Id = id;
Name = name;
Prop = prop;
enabled = isenabled;
}
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
When I change this <Style TargetType="sdk:DataGridRow"> part with "local:CustomGrid" my entire grid just go blank not to sure why.
Is there any advice on how to accomplish this or maybe a different method?
Try something like this:
public class CustomGrid : DataGrid
{
private List<Model> models;
public CustomGrid()
{
Loaded += (s,e) => models = ItemsSource as List<Model>;
}
protected override void OnKeyDown(KeyEventArgs e)
{
if (models == null)
return;
Model model = CurrentItem as Model;
if (model == null)
return;
int index = models.IndexOf(model);
switch (e.Key)
{
case Key.Down:
//is the next model disabled?
if (index < models.Count - 1 && !models[index + 1].enabled)
e.Handled = true;
break;
case Key.Up:
if (index > 0 && !models[index - 1].enabled)
e.Handled = true;
break;
}
}
}
XAML:
<UserControl ...>
<UserControl.Resources>
<Style x:Key="DataGridStyle1" TargetType="local:CustomGrid">
<Setter Property="RowStyle">
<Setter.Value>
<Style TargetType="sdk:DataGridRow">
<Setter Property="IsEnabled" Value="{Binding enabled}"/>
</Style>
</Setter.Value>
</Setter>
</Style>
</UserControl.Resources>
<Grid x:Name="LayoutRoot" Background="White">
<local:CustomGrid x:Name="McDataGrid" Style="{StaticResource DataGridStyle1}" />
</Grid>
</UserControl>
I have a ComboBox which should have a text when nothing is selected. This seems like a straight forward problem, and has many answers on the net, but unfortunately, it is not working for me. I think, the reason is, that I don't want to show a static text, but rather a bound text.
My minimal not working example looks like this:
public class Model
{
public string Name { get; set; }
public SubModel SelectedItem { get; set; }
public List<SubModel> Items { get; set; }
}
public class SubModel
{
public string Description { get; set; }
}
and the MainWindow:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var selectedSubModel = new SubModel { Description = "SubModel5" };
var model1 = new Model
{
Name = "Model1",
Items = new List<SubModel>
{
new SubModel { Description = "SubModel1" },
new SubModel { Description = "SubModel2" },
new SubModel { Description = "SubModel3" }
}
};
var model2 = new Model
{
Name = "Model2",
SelectedItem = selectedSubModel,
Items = new List<SubModel>
{
new SubModel { Description = "SubModel4" },
selectedSubModel,
new SubModel { Description = "SubModel6" }
}
};
var model3 = new Model
{
Name = "Model3",
Items = new List<SubModel>
{
new SubModel { Description = "SubModel7" },
new SubModel { Description = "SubModel8" },
new SubModel { Description = "SubModel9" }
}
};
_itemsControl.Items.Add(model1);
_itemsControl.Items.Add(model2);
_itemsControl.Items.Add(model3);
}
}
with xaml:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:WpfApplication1="clr-namespace:WpfApplication1"
Title="MainWindow" Height="350" Width="525">
<ItemsControl x:Name="_itemsControl">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="WpfApplication1:Model">
<ComboBox ItemsSource="{Binding Items}"
SelectedItem="{Binding SelectedItem}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Description}"></TextBlock>
</DataTemplate>
</ComboBox.ItemTemplate>
<ComboBox.Style>
<Style TargetType="ComboBox">
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedItem}" Value="{x:Null}">
<Setter Property="Background">
<Setter.Value>
<VisualBrush>
<VisualBrush.Visual>
<TextBlock Text="{Binding Name}"/>
</VisualBrush.Visual>
</VisualBrush>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</ComboBox.Style>
</ComboBox>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Window>
This gives the follwing:
But it should look similar to this:
please first of all take in your mind facts provided in the next sentence - you can only select items provided by ComboBox ItemsSource. Thus since the Name property values (Model1, Model2, Model3 etc.) are not in your collection they can't be selected you will see the empty selection Instead. I can suggest you the next solution the combination of data context proxy and wpf behavior.
Xaml code
<Window x:Class="ComboBoxWhenNoAnySelectedHelpAttempt.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:comboBoxWhenNoAnySelectedHelpAttempt="clr-namespace:ComboBoxWhenNoAnySelectedHelpAttempt"
xmlns:system="clr-namespace:System;assembly=mscorlib"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
Title="MainWindow" Height="350" Width="525">
<Grid>
<ItemsControl x:Name="_itemsControl">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="comboBoxWhenNoAnySelectedHelpAttempt:Model">
<ComboBox x:Name="ComboBox"
SelectedItem="{Binding SelectedItem, UpdateSourceTrigger=PropertyChanged}">
<ComboBox.Resources>
<!--the next object is a proxy that able to provide combo data context each time it requested-->
<comboBoxWhenNoAnySelectedHelpAttempt:FreezableProxyClass x:Key="FreezableProxyClass" ProxiedDataContext="{Binding ElementName=ComboBox, Path=DataContext }"></comboBoxWhenNoAnySelectedHelpAttempt:FreezableProxyClass>
</ComboBox.Resources>
<ComboBox.ItemsSource>
<CompositeCollection>
<!--the next object is a collapsed combo box that can be selected in code-->
<!--keep im mind, since this object is not a SubModel we get the binding expression in output window-->
<ComboBoxItem IsEnabled="False" Visibility="Collapsed" Foreground="Black" Content="{Binding Source={StaticResource FreezableProxyClass},
Path=ProxiedDataContext.Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"></ComboBoxItem>
<CollectionContainer Collection="{Binding Source={StaticResource FreezableProxyClass},
Path=ProxiedDataContext.Items, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</CompositeCollection>
</ComboBox.ItemsSource>
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Description}"></TextBlock>
</DataTemplate>
</ComboBox.ItemTemplate>
<i:Interaction.Behaviors>
<!--next behavior helps to select a zero index (Model.Name collapsed) item from source when selected item is not SubModel-->
<comboBoxWhenNoAnySelectedHelpAttempt:ComboBoxLoadingBehavior/>
</i:Interaction.Behaviors>
</ComboBox>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
Code Behind with Proxy Code
public class FreezableProxyClass : Freezable
{
protected override Freezable CreateInstanceCore()
{
return new FreezableProxyClass();
}
public static readonly DependencyProperty ProxiedDataContextProperty = DependencyProperty.Register(
"ProxiedDataContext", typeof(object), typeof(FreezableProxyClass), new PropertyMetadata(default(object)));
public object ProxiedDataContext
{
get { return (object)GetValue(ProxiedDataContextProperty); }
set { SetValue(ProxiedDataContextProperty, value); }
}
}
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var selectedSubModel = new SubModel { Description = "SubModel5" };
var model1 = new Model
{
Name = "Model1",
Items = new ObservableCollection<SubModel>
{
new SubModel { Description = "SubModel1" },
new SubModel { Description = "SubModel2" },
new SubModel { Description = "SubModel3" }
}
};
var model2 = new Model
{
Name = "Model2",
SelectedItem = selectedSubModel,
Items = new ObservableCollection<SubModel>
{
new SubModel { Description = "SubModel4" },
selectedSubModel,
new SubModel { Description = "SubModel6" }
}
};
var model3 = new Model
{
Name = "Model3",
Items = new ObservableCollection<SubModel>
{
new SubModel { Description = "SubModel7" },
new SubModel { Description = "SubModel8" },
new SubModel { Description = "SubModel9" }
}
};
_itemsControl.Items.Add(model1);
_itemsControl.Items.Add(model2);
_itemsControl.Items.Add(model3);
}
}
public class Model:BaseObservableObject
{
private string _name;
private SubModel _selectedItem;
private ObservableCollection<SubModel> _items;
public string Name
{
get { return _name; }
set
{
_name = value;
OnPropertyChanged();
}
}
public SubModel SelectedItem
{
get { return _selectedItem; }
set
{
_selectedItem = value;
OnPropertyChanged();
}
}
public ObservableCollection<SubModel> Items
{
get { return _items; }
set
{
_items = value;
OnPropertyChanged();
}
}
}
public class SubModel:BaseObservableObject
{
private string _description;
public string Description
{
get { return _description; }
set
{
_description = value;
OnPropertyChanged();
}
}
}
BaseObservableObject code (simple implementation for INotifyPropertyChanged )
/// <summary>
/// implements the INotifyPropertyChanged (.net 4.5)
/// </summary>
public class BaseObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
var handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
protected virtual void OnPropertyChanged<T>(Expression<Func<T>> raiser)
{
var propName = ((MemberExpression)raiser.Body).Member.Name;
OnPropertyChanged(propName);
}
protected bool Set<T>(ref T field, T value, [CallerMemberName] string name = null)
{
if (!EqualityComparer<T>.Default.Equals(field, value))
{
field = value;
OnPropertyChanged(name);
return true;
}
return false;
}
}
WPF Behavior Code
public class ComboBoxLoadingBehavior:Behavior<ComboBox>
{
private bool _unLoaded;
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.Loaded += AssociatedObjectOnLoaded;
AssociatedObject.LayoutUpdated += AssociatedObjectOnLayoutUpdated;
AssociatedObject.Unloaded += AssociatedObjectOnUnloaded;
}
private void AssociatedObjectOnUnloaded(object sender, RoutedEventArgs routedEventArgs)
{
_unLoaded = true;
UnsubscribeAll();
}
private void UnsubscribeAll()
{
AssociatedObject.Loaded -= AssociatedObjectOnLoaded;
AssociatedObject.LayoutUpdated -= AssociatedObjectOnLayoutUpdated;
AssociatedObject.Unloaded -= AssociatedObjectOnUnloaded;
}
private void AssociatedObjectOnLayoutUpdated(object sender, EventArgs eventArgs)
{
UpdateSelectionState(sender);
}
private static void UpdateSelectionState(object sender)
{
var combo = sender as ComboBox;
if (combo == null) return;
var selectedItem = combo.SelectedItem as SubModel;
if (selectedItem == null)
{
combo.SelectedIndex = 0;
}
}
private void AssociatedObjectOnLoaded(object sender, RoutedEventArgs routedEventArgs)
{
_unLoaded = false;
UpdateSelectionState(sender);
}
protected override void OnDetaching()
{
base.OnDetaching();
if(_unLoaded) return;
UnsubscribeAll();
}
}
This is a working complete solution for you problem, just copy/past and use it as a starting point for your farther research. I'll glad to help if you will have any problems with the code.
Regards.
I've found 2 possible solutions:
Change ComboBox template
Edit standard combobox template by right button click on combobox in designer and select Edit Template -> Edit a Copy...
After that change ContentPresenter with a custom converter:
XAML
<ContentPresenter ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}" ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}" ContentStringFormat="{TemplateBinding SelectionBoxItemStringFormat}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" IsHitTestVisible="false" Margin="{TemplateBinding Padding}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}">
<ContentPresenter.Content>
<MultiBinding Converter="{local:ComboboxEmptyValueConverter}">
<Binding Path="SelectionBoxItem" RelativeSource="{RelativeSource Mode=TemplatedParent}" />
<Binding Mode="OneWay" Path="DataContext" RelativeSource="{RelativeSource Mode=TemplatedParent}" />
</MultiBinding>
</ContentPresenter.Content>
</ContentPresenter>
C#
class ComboboxEmptyValueConverterExtension : MarkupExtension, IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
string stringValue = values[0] as string;
var dataContext = values[1] as Model;
return (stringValue != null && String.IsNullOrEmpty(stringValue)) ? dataContext?.Name : values[0];
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
return new object[] { value, null };
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
return this;
}
}
Set ComboBox to IsEditable & IsReadOnly and change Text
<ComboBox ItemsSource="{Binding Items}" SelectedItem="{Binding SelectedItem}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock x:Name="textBlock" Text="{Binding Description}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
<ComboBox.Style>
<Style TargetType="ComboBox">
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedItem}" Value="{x:Null}">
<Setter Property="IsEditable" Value="True" />
<Setter Property="IsReadOnly" Value="True" />
<Setter Property="Text" Value="{Binding Name}" />
</DataTrigger>
</Style.Triggers>
</Style>
</ComboBox.Style>
</ComboBox>
The answer is, to put the visual brush in the resources of the combobox:
<DataTemplate DataType="WpfApplication1:Model">
<ComboBox ItemsSource="{Binding Items}"
SelectedItem="{Binding SelectedItem}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Description}"></TextBlock>
</DataTemplate>
</ComboBox.ItemTemplate>
<ComboBox.Resources>
<VisualBrush x:Key="_myBrush">
<VisualBrush.Visual>
<TextBlock Text="{Binding Name}"/>
</VisualBrush.Visual>
</VisualBrush>
</ComboBox.Resources>
<ComboBox.Style>
<Style TargetType="ComboBox">
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedItem}" Value="{x:Null}">
<Setter Property="Background" Value="{StaticResource _myBrush}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ComboBox.Style>
</ComboBox>
</DataTemplate>
Then, together with the rest of the code, it works like expected.
I am trying to create a File Explorer style TreeView/ListView window that will allow the user to select a "Project" from within a "ProjectFolder".
The "ProjectFolder" tree is bound to a property on my ViewModel called "RootProjectFolders" and the "Project" list is bound to a property called "ProjectsInSelectedFolder". Things were mostly working; however, I was getting null exceptions when I first loaded the window because the "SelectedFolder" had not yet been set. When I tried to implement a simple check to make sure that the "SelectedFolder" was not null, my "Project" ListView stopped refreshing.
if ((this.SelectedFolder != null) && (this.SelectedFolder.ProjectFolder.Projects != null))
{
foreach (Project project in this.SelectedFolder.ProjectFolder.Projects)
{
_projectsInSelectedFolder.Add(new ProjectViewModel(project));
}
}
base.RaisePropertyChangedEvent("ProjectsInSelectedFolder");
If I remove (this.SelectedFolder != null) from the above, the ListView will update, but I will get an NullException error. Why is that check breaking my binding?
Following up on the request for additional information, here is the XAML of the TreeView and ListView that are binding to the properties on the ViewModel:
<TreeView Name="treeviewProjectFolders" Grid.Column="0"
ItemsSource="{Binding Path=RootProjectFolders}">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
<Setter Property="FontWeight" Value="Normal" />
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="FontWeight" Value="Bold" />
</Trigger>
</Style.Triggers>
</Style>
</TreeView.ItemContainerStyle>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate
ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Path=Name}" />
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
<GridSplitter Name="splitterProjects" Grid.Column="1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" />
<ListView Name="listviewProjects" Grid.Column="2" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
ItemsSource="{Binding Path=ProjectsInSelectedFolder}">
<ListView.ItemContainerStyle>
<Style TargetType="{x:Type ListViewItem}">
<Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=TwoWay}" />
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=Name}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
And here is the ViewModel
public class SelectProjectViewModel : ViewModelBase
{
#region Fields
List<ProjectViewModel> _projectsInSelectedFolder;
List<ProjectFolderViewModel> _rootProjectFolders;
static ProjectFolderViewModel _selectedFolder = null;
ProjectViewModel _selectedProject;
#endregion // Fields
#region Constructor
public SelectProjectViewModel(ProjectFolders rootProjectFolders)
{
if (_rootProjectFolders != null) { _rootProjectFolders.Clear(); }
_rootProjectFolders = new List<ProjectFolderViewModel>();
foreach (ProjectFolder rootFolder in rootProjectFolders)
{
_rootProjectFolders.Add(new ProjectFolderViewModel(rootFolder, this));
}
_projectsInSelectedFolder = new List<ProjectViewModel>();
// Subscribe to events
this.PropertyChanged += OnPropertyChanged;
}
#endregion // Constructor
#region Properties
public List<ProjectFolderViewModel> RootProjectFolders
{
get
{
return _rootProjectFolders;
}
}
public List<ProjectViewModel> ProjectsInSelectedFolder
{
get
{
return _projectsInSelectedFolder;
}
}
public ProjectFolderViewModel SelectedFolder
{
get
{
return _selectedFolder;
}
set
{
if (_selectedFolder != value)
{
_selectedFolder = value;
}
}
}
public ProjectViewModel SelectedProject
{
get
{
return _selectedProject;
}
set
{
_selectedProject = value;
base.RaisePropertyChangedEvent("SelectedProject");
}
}
#endregion // Properties
#region Methods
public void FindSelectedFolder(ProjectFolderViewModel root)
{
if (root.IsSelected) { _selectedFolder = root; }
else
{
foreach (ProjectFolderViewModel folder in root.Children)
{
if (_selectedFolder == null)
{
FindSelectedFolder(folder);
}
}
}
}
#endregion // Methods
#region Event Handlers
void OnPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case "SelectedFolder":
_selectedFolder = null;
foreach (ProjectFolderViewModel root in this.RootProjectFolders)
{
if (_selectedFolder == null)
{
this.FindSelectedFolder(root);
}
}
_projectsInSelectedFolder.Clear();
if ((this.SelectedFolder != null) && (this.SelectedFolder.ProjectFolder.Projects != null))
{
foreach (Project project in this.SelectedFolder.ProjectFolder.Projects)
{
_projectsInSelectedFolder.Add(new ProjectViewModel(project));
}
}
base.RaisePropertyChangedEvent("ProjectsInSelectedFolder");
break;
}
}
#endregion // Event Handlers
Also, here is the ViewModel for the individual project folders that are used to raise the "SelectedFolder" property:
public class ProjectFolderViewModel : ViewModelBase
{
#region Fields
ReadOnlyCollection<ProjectFolderViewModel> _children;
List<ProjectFolderViewModel> _childrenList;
bool _isExpanded;
bool _isSelected;
ProjectFolderViewModel _parentNode;
SelectProjectViewModel _parentTree;
ProjectFolder _projectFolder;
#endregion // Fields
#region Constructor
public ProjectFolderViewModel(ProjectFolder projectFolder, SelectProjectViewModel parentTree) : this(projectFolder, parentTree, null)
{ }
private ProjectFolderViewModel(ProjectFolder projectFolder, SelectProjectViewModel parentTree, ProjectFolderViewModel parentNode)
{
_projectFolder = projectFolder;
_parentTree = parentTree;
_parentNode = parentNode;
_childrenList = new List<ProjectFolderViewModel>();
foreach (ProjectFolder child in _projectFolder.ChildFolders)
{
_childrenList.Add(new ProjectFolderViewModel(child, _parentTree));
}
_children = new ReadOnlyCollection<ProjectFolderViewModel>(_childrenList);
}
#endregion // Constructor
#region Properties
public ReadOnlyCollection<ProjectFolderViewModel> Children
{
get
{
return _children;
}
}
public bool IsExpanded
{
get
{
return _isExpanded;
}
set
{
if (value != _isExpanded)
{
_isExpanded = value;
this.OnPropertyChanged("IsExpanded");
}
// Expand all the way up to the root.
if (_isExpanded && _parentNode != null)
_parentNode.IsExpanded = true;
}
}
public bool IsSelected
{
get
{
return _isSelected;
}
set
{
_isSelected = value;
base.RaisePropertyChangedEvent("IsSelected");
//if (_isSelected)
//{
_parentTree.RaisePropertyChangedEvent("SelectedFolder");
//}
}
}
public string Name
{
get
{
return _projectFolder.Name;
}
}
public ProjectFolder ProjectFolder
{
get
{
return _projectFolder;
}
}
#endregion // Properties
Change all your
List<T> to observablecollection<T>
because when ever there is new file or folder your adding the Item, your not creating new List, since observablecollection implements INotifyCollectionChanged, and INotifyPropertyChanged it'll internally take care of notifying and refreshing the View. But list cant do that