WPF TreeView leaking the selected item - c#

I currently have a strange memory leak with WPF TreeView. When I select an item in the TreeView, the corresponding bound ViewModel is strongely hold in the TreeView EffectiveValueEntry[] collection. The issue is that it is not released when the ViewModel is removed from it's parent collection.
Here is a simple code to reproduce the issue :
MainWindow.xaml
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls.Primitives;
namespace TreeViewMemoryLeak
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = this;
}
public ObservableCollection<Entry> Entries
{
get
{
if (entries == null)
{
entries = new ObservableCollection<Entry>() { new Entry() { DisplayName = "First Entry" } };
}
return entries;
}
}
private void Button_Click(object sender, RoutedEventArgs e) { entries.Clear(); }
private ObservableCollection<Entry> entries;
}
public class Entry : DependencyObject
{
public string DisplayName { get; set; }
}
}
MainWindow.xaml.cs
<Window x:Class="TreeViewMemoryLeak.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:TreeViewMemoryLeak"
Title="MainWindow" Height="350" Width="250">
<Window.Resources>
<DataTemplate DataType="{x:Type local:Entry}">
<TextBlock Text="{Binding DisplayName}" />
</DataTemplate>
</Window.Resources>
<StackPanel>
<Button Content="delete item" Click="Button_Click" Grid.Row="0" Margin="10"/>
<TreeView x:Name="treeView" ItemsSource="{Binding Entries}" Grid.Row="1" Margin="10" BorderBrush="Black" BorderThickness="1" />
</StackPanel>
</Window>
To reproduce the issue
Select the item, then click the button to clear the ObservableCollection. Now check the EffectiveValueEntry[] on the TreeView control : the ViewModel is still there and is not flagged for garbage collection.

Well I finally came up with a rather violent solution. I remove the reference from the EffectiveValues collection myself when deleting the last object in the TreeView. It may be overkill but at least, it works.
public class MyTreeView : TreeView
{
protected override void OnSelectedItemChanged(RoutedPropertyChangedEventArgs<object> e)
{
base.OnSelectedItemChanged(e);
if (Items.Count == 0)
{
var lastObjectDeleted = e.OldValue;
if (lastObjectDeleted != null)
{
var effectiveValues = EffectiveValuesGetMethod.Invoke(this, null) as Array;
if (effectiveValues == null)
throw new InvalidOperationException();
bool foundEntry = false;
int index = 0;
foreach (var effectiveValueEntry in effectiveValues)
{
var value = EffectiveValueEntryValueGetMethod.Invoke(effectiveValueEntry, null);
if (value == lastObjectDeleted)
{
foundEntry = true;
break;
}
index++;
}
if (foundEntry)
{
effectiveValues.SetValue(null, index);
}
}
}
}
protected MethodInfo EffectiveValueEntryValueGetMethod
{
get
{
if (effectiveValueEntryValueGetMethod == null)
{
var effectiveValueEntryType = AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes()).Where(t => t.Name == "EffectiveValueEntry").FirstOrDefault();
if (effectiveValueEntryType == null)
throw new InvalidOperationException();
var effectiveValueEntryValuePropertyInfo = effectiveValueEntryType.GetProperty("Value", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.DeclaredOnly | System.Reflection.BindingFlags.Instance);
if (effectiveValueEntryValuePropertyInfo == null)
throw new InvalidOperationException();
effectiveValueEntryValueGetMethod = effectiveValueEntryValuePropertyInfo.GetGetMethod(nonPublic: true);
if (effectiveValueEntryValueGetMethod == null)
throw new InvalidOperationException();
}
return effectiveValueEntryValueGetMethod;
}
}
protected MethodInfo EffectiveValuesGetMethod
{
get
{
if (effectiveValuesGetMethod == null)
{
var dependencyObjectType = typeof(DependencyObject);
var effectiveValuesPropertyInfo = dependencyObjectType.GetProperty("EffectiveValues", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.DeclaredOnly | System.Reflection.BindingFlags.Instance);
if (effectiveValuesPropertyInfo == null)
throw new InvalidOperationException();
effectiveValuesGetMethod = effectiveValuesPropertyInfo.GetGetMethod(nonPublic: true);
if (effectiveValuesGetMethod == null)
throw new InvalidOperationException();
}
return effectiveValuesGetMethod;
}
}
#region Private fields
private MethodInfo effectiveValueEntryValueGetMethod;
private MethodInfo effectiveValuesGetMethod;
#endregion
}

It is because you binded your treeview with OneTime mode, so your collection was 'snapshotted'. As stated:
UPDATED:
EffectiveValueEntry is about how DependencyObjects store the values of their DependencyProperties. This collection will hold object as long as treeView has selected item. As soon as you select something else collection will be updated.

I had the same issue and solved it using one of the solutions on this link (posted by Tom Goff). Do the following:
ClearSelection(this.treeView);
this.treeView.SelectedValuePath = ".";
this.treeView.ClearValue(TreeView.SelectedValuePathProperty);
this.treeView.ItemsSource = null;
...
public static void ClearSelection(TreeView treeView)
{
if (treeView != null)
ClearSelection(treeView.Items, treeView.ItemContainerGenerator);
}
private static void ClearSelection(ItemCollection collection, ItemContainerGenerator generator)
{
if ((collection != null) && (generator != null))
{
for (int i = 0; i < collection.Count; i++)
{
TreeViewItem treeViewItem = generator.ContainerFromIndex(i) as TreeViewItem;
if (treeViewItem != null)
{
ClearSelection(treeViewItem.Items, treeViewItem.ItemContainerGenerator);
treeViewItem.IsSelected = false;
}
}
}
}

Related

ComboBox, is it possible to have ItemsSource and SelectedValue bound to different sources?

I have two different objects that are pointing at each other. The first object represents a division in a company. That object has two collection: Employees, which is all the employees working in the division and Project, which is all the special projects that are in progress within that division. So the first object looks like this:
public class Division : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged = delegate { };
ObservableCollection<Employee> _employees;
ObservableCollection<Project> _projects;
public Division()
{
Employees = new ObservableCollection<Employee>();
Projects = new ObservableCollection<Project>();
}
public ObservableCollection<Employee> Employees
{
get { return _employees; }
set
{
if (_employees != value)
{
_employees = value;
PropertyChanged(this, new PropertyChangedEventArgs("Employees"));
}
}
}
public ObservableCollection<Project> Projects
{
get { return _projects; }
set
{
if (_projects != value)
{
_projects = value;
PropertyChanged(this, new PropertyChangedEventArgs("Projects"));
}
}
}
public void AddNewProject()
{
this.Projects.Add(new Project(this));
}
}
Notice that when adding a new project to the division, I pass a reference to the division into that project, which looks like this:
public class Project : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged = delegate { };
string _projectName;
DateTime _deadline = DateTime.Now;
Division _division;
ObservableCollection<Employee> _members;
public Project()
{
Members = new ObservableCollection<Employee>();
}
public Project(Division div)
{
Members = new ObservableCollection<Employee>();
Division = div;
}
public string ProjectName
{
get { return _projectName; }
set
{
if (_projectName != value)
{
_projectName = value;
PropertyChanged(this, new PropertyChangedEventArgs("ProjectName"));
}
}
}
public DateTime Deadline
{
get { return _deadline; }
set
{
if (_deadline != value)
{
_deadline = value;
PropertyChanged(this, new PropertyChangedEventArgs("Deadline"));
}
}
}
public Division Division
{
get { return _division; }
set
{
if (_division != value)
{
if (_division != null)
{
_division.Employees.CollectionChanged -= members_CollectionChanged;
}
_division = value;
if (_division != null)
{
_division.Employees.CollectionChanged += members_CollectionChanged;
}
PropertyChanged(this, new PropertyChangedEventArgs("Division"));
}
}
}
public ObservableCollection<Employee> Members
{
get { return _members; }
set
{
if (_members != value)
{
if (_members != null)
{
_members.CollectionChanged -= members_CollectionChanged;
}
_members = value;
if (_members != null)
{
_members.CollectionChanged += members_CollectionChanged;
}
PropertyChanged(this, new PropertyChangedEventArgs("Members"));
}
}
}
public ObservableCollection<Employee> AvailableEmployees
{
get
{
if (Division != null){
IEnumerable<Employee> availables =
from s in Division.Employees
where !Members.Contains(s)
select s;
return new ObservableCollection<Employee>(availables);
}
return new ObservableCollection<Employee>();
}
}
void members_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
PropertyChanged(this, new PropertyChangedEventArgs("AvailableEmployees"));
}
}
The reason I'm doing it like this is, that the project could have any type of team working on it, but only from within the division. So, when building a dashboard for the division, the manager could select any of the employees to that project but without putting in an employee that is already assigned to it. So, the AvailableEmployees property in the project object always keeps track of who is not already assigned to that project.
The problem I'm having is how to translate this into a UI. The experiment I've done so far looks like this:
<UserControl x:Class="Test.Views.TestView"
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"
mc:Ignorable="d"
xmlns:local="clr-namespace:Test.Views"
d:DesignHeight="300" d:DesignWidth="300">
<StackPanel>
<ListBox ItemsSource="{Binding Div.Projects}">
<ListBox.ItemTemplate>
<DataTemplate>
<Border Background="Transparent"
BorderThickness="0, 0, 0, 2"
BorderBrush="Black"
Margin="0, 0, 0, 5"
Padding="0, 0, 0, 5">
<StackPanel>
<TextBox Text="{Binding ProjectName}"/>
<ListBox ItemsSource="{Binding Members}">
<ListBox.ItemTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding RelativeSource={RelativeSource AncestorType=local:TestView}, Path=DataContext.AvailableEmployees}"
DisplayMemberPath="FirstName"
Text="{Binding FirstName}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button Content="Add Employee to Project"
Command="{Binding RelativeSource={RelativeSource AncestorType=local:TestView}, Path=DataContext.AddEmployeeToProject}"
CommandParameter="{Binding}"/>
</StackPanel>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button Content="Add New Project"
Command="{Binding AddNewProject}" />
</StackPanel>
The view model associated with this view is as follows:
public class TestViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged = delegate { };
private Division _div;
public TestViewModel(Division div)
{
Div = div;
AddNewProject = new DelegateCommand(OnAddNewProject);
AddEmployeeToProject = new DelegateCommand<Project>(OnAddEmployeeToProject);
}
public DelegateCommand AddNewProject { get; set; }
public DelegateCommand<Project> AddEmployeeToProject { get; set; }
public Division Div
{
get { return _div; }
set
{
if (_div != value)
{
_div = value;
PropertyChanged(this, new PropertyChangedEventArgs("Div"));
}
}
}
private void OnAddNewProject()
{
Div.AddNewProject();
}
private void OnAddEmployeeToProject(Project proj)
{
var availables = proj.AvailableEmployees;
if (availables.Count > 0)
{
proj.Members.Add(availables[0]);
}
}
}
However, I cannot get the combobox for each employee in each project to work. It seems like the selected item/value is bound to the itemssource, and each time the combobox turns out blank. I've tried to do this also with SelectedValue and SelectedItem properties for the combobox, but none worked.
How do I get these two separated. Is there anything else I'm missing here?
OK. After so many experiments the best solution I came up with was to create my own user control that is composed of both a button and a combobox that imitate the behavior I was expecting of the combobox on it own.
First, I had a really stupid mistake in the model where both lists of members Project and Division contain the same instances of Employee, which makes the AvailableEmployees property buggy. What I really needed to do is to create a list of copies of employees in the Project instead of just references.
In any case, I created a new user control and called it DynamicSourceComboBox. The XAML of this control looks like this:
<Grid>
<Button x:Name="selected"
Content="{Binding RelativeSource={RelativeSource AncestorType=local:DynamicSourceComboBox}, Path=SelectedValue}"
Click="selected_Click"/>
<ComboBox x:Name="selections"
ItemsSource="{Binding RelativeSource={RelativeSource AncestorType=local:DynamicSourceComboBox}, Path=ItemsSource}"
DisplayMemberPath="{Binding RelativeSource={RelativeSource AncestorType=local:DynamicSourceComboBox}, Path=DisplayMemberPath}"
Visibility="Collapsed"
SelectionChanged="selections_SelectionChanged"
MouseLeave="selections_MouseLeave"/>
</Grid>
I have here a few bindings from the button and the combobox to properties in my user control. These are actually dependency properties. The code-behind of my user control looks like this:
public partial class DynamicSourceComboBox : UserControl
{
public DynamicSourceComboBox()
{
InitializeComponent();
}
public object SelectedValue
{
get { return (object)GetValue(SelectedValueProperty); }
set { SetValue(SelectedValueProperty, value); }
}
public static readonly DependencyProperty SelectedValueProperty =
DependencyProperty.Register("SelectedValue", typeof(object), typeof(DynamicSourceComboBox), new PropertyMetadata(null));
public IEnumerable ItemsSource
{
get { return (IEnumerable)GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
public static readonly DependencyProperty ItemsSourceProperty =
ComboBox.ItemsSourceProperty.AddOwner(typeof(DynamicSourceComboBox));
public string DisplayMemberPath
{
get { return (string)GetValue(DisplayMemberPathProperty); }
set { SetValue(DisplayMemberPathProperty, value); }
}
public static readonly DependencyProperty DisplayMemberPathProperty =
ComboBox.DisplayMemberPathProperty.AddOwner(typeof(DynamicSourceComboBox));
private void selected_Click(object sender, RoutedEventArgs e)
{
selected.Visibility = Visibility.Hidden;
selections.Visibility = Visibility.Visible;
selections.IsDropDownOpen = true;
}
private void selections_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
selections.Visibility = Visibility.Collapsed;
selected.Visibility = Visibility.Visible;
selections.IsDropDownOpen = false;
if (e.AddedItems.Count == 1)
{
var item = e.AddedItems[0];
Type itemType = item.GetType();
var itemTypeProps = itemType.GetProperties();
var realValue = (from prop in itemTypeProps
where prop.Name == DisplayMemberPath
select prop.GetValue(selections.SelectedValue)).First();
SelectedValue = realValue;
}
}
private void selections_MouseLeave(object sender, MouseEventArgs e)
{
selections.Visibility = Visibility.Collapsed;
selected.Visibility = Visibility.Visible;
selections.IsDropDownOpen = false;
}
}
These dependency properties imitate the properties with similar names in ComboBox but they are hooked up to the internal combobox and the button in a way that makes them behave together as a single complex combobox.
The Click event in the button hides it and present the combobox to make the effect of just a box that is opening. Then I have a SelectionChanged event in the combobox firing to update all the needed information and a MouseLeave event just in case the user doesn't make any real selection change.
When I need to use the new user control, I set it up like this:
<local:DynamicSourceComboBox ItemsSource="{Binding RelativeSource={RelativeSource AncestorLevel=1, AncestorType=ListBox}, Path=DataContext.AvailableEmployees}"
DisplayMemberPath="FirstName"
SelectedValue="{Binding FirstName, Mode=TwoWay}"/>
Of course, for all of it to work, I have to make a lot of hookups with PropertyChanged events in the models, so the Projects instance will know to raise a PropertyChanged event for AvailableEmployees any time a change is made, but this is not really the concern of this user control itself.
This is a pretty clunky solution, with a lot of extra code that is a bit hard to follow, but it's really the best (actually only) solution I could have come up with to the problem I had.

How to handle `ScrollViewer.ScrollChanged` event in MVVM?

I've tried to handle the routed event ScrollViewer.ScrollChanged of DataGrid in obvious way:
<i:Interaction.Triggers>
<i:EventTrigger EventName="ScrollViewer.ScrollChanged">
<ei:CallMethodAction MethodName="ScrollChangedHandler" TargetObject="{Binding}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
But ScrollChangedHandler did not even fired.
Then, I've found this article about handling events, but I could not figure out what xml namespace(xmlns) is used for mvvmjaco:
<Image Width="360" Height="177" Source="Resources\PlayerArea.png">
<i:Interaction.Triggers>
<mvvmjoy:RoutedEventTrigger RoutedEvent="s:Contacts.ContactDown">
<mvvmjaco:CommandAction Command="{Binding TouchCommand}" />
</mvvmjoy:RoutedEventTrigger>
</i:Interaction.Triggers>
</Image>
mvvmjoy uses this class from the article:
public class RoutedEventTrigger :EventTriggerBase<DependencyObject>
{
RoutedEvent _routedEvent;
//The code omitted for the brevity
}
Basically, I have two questions:
What class or library should I use for mvvmjaco xml namespace?
How I can handle ScrollViewer.ScrollChanged event in my viewModel with its arguments?
I would solve it with the following Attached-Property:
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace WpfApplication2
{
public class DataGridExtensions
{
public static readonly DependencyProperty ScrollChangedCommandProperty = DependencyProperty.RegisterAttached(
"ScrollChangedCommand", typeof(ICommand), typeof(DataGridExtensions),
new PropertyMetadata(default(ICommand), OnScrollChangedCommandChanged));
private static void OnScrollChangedCommandChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
DataGrid dataGrid = d as DataGrid;
if (dataGrid == null)
return;
if (e.NewValue != null)
{
dataGrid.Loaded += DataGridOnLoaded;
}
else if (e.OldValue != null)
{
dataGrid.Loaded -= DataGridOnLoaded;
}
}
private static void DataGridOnLoaded(object sender, RoutedEventArgs routedEventArgs)
{
DataGrid dataGrid = sender as DataGrid;
if (dataGrid == null)
return;
ScrollViewer scrollViewer = UIHelper.FindChildren<ScrollViewer>(dataGrid).FirstOrDefault();
if (scrollViewer != null)
{
scrollViewer.ScrollChanged += ScrollViewerOnScrollChanged;
}
}
private static void ScrollViewerOnScrollChanged(object sender, ScrollChangedEventArgs e)
{
DataGrid dataGrid = UIHelper.FindParent<DataGrid>(sender as ScrollViewer);
if (dataGrid != null)
{
ICommand command = GetScrollChangedCommand(dataGrid);
command.Execute(e);
}
}
public static void SetScrollChangedCommand(DependencyObject element, ICommand value)
{
element.SetValue(ScrollChangedCommandProperty, value);
}
public static ICommand GetScrollChangedCommand(DependencyObject element)
{
return (ICommand)element.GetValue(ScrollChangedCommandProperty);
}
}
}
The class UIHelper looks like:
using System.Collections.Generic;
using System.Windows;
using System.Windows.Media;
namespace WpfApplication2
{
internal static class UIHelper
{
internal static IList<T> FindChildren<T>(DependencyObject element) where T : FrameworkElement
{
List<T> retval = new List<T>();
for (int counter = 0; counter < VisualTreeHelper.GetChildrenCount(element); counter++)
{
FrameworkElement toadd = VisualTreeHelper.GetChild(element, counter) as FrameworkElement;
if (toadd != null)
{
T correctlyTyped = toadd as T;
if (correctlyTyped != null)
{
retval.Add(correctlyTyped);
}
else
{
retval.AddRange(FindChildren<T>(toadd));
}
}
}
return retval;
}
internal static T FindParent<T>(DependencyObject element) where T : FrameworkElement
{
FrameworkElement parent = VisualTreeHelper.GetParent(element) as FrameworkElement;
while (parent != null)
{
T correctlyTyped = parent as T;
if (correctlyTyped != null)
{
return correctlyTyped;
}
return FindParent<T>(parent);
}
return null;
}
}
}
Then you can write in the definition of your DataGrid:
<DataGrid ItemsSource="{Binding MySource}" extensionsNamespace:DataGridExtensions.ScrollChangedCommand="{Binding ScrollCommand}"/>
And in your ViewModel you have an ICommand which looks like:
private ICommand scrollCommand;
public ICommand ScrollCommand
{
get { return scrollCommand ?? (scrollCommand = new RelayCommand(Scroll)); }
}
private void Scroll(object parameter)
{
ScrollChangedEventArgs scrollChangedEventArgs = parameter as ScrollChangedEventArgs;
if (scrollChangedEventArgs != null)
{
}
}
For the first your question(special thanks to Andy ONeill and Magnus Montin ):
MVVMJaco is xmlns:mvvmjaco="galasoft.ch/mvvmlight"
And the libraries needed are:
GalaSoft.MVVmLight
GalaSoft.MVVmLight.Extras
GalaSoft.MVVmLight.Platform
Seems that mvvmjaco:CommandAction is an action to invoke command from your ViewModel. You can use i:InvokeCommandAction as substitute.
You can use RoutedEventTrigger from the article you've linked to handle scroll changed event.
XAML:
<Window x:Class="ScrollChangedTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:ScrollChangedTest"
mc:Ignorable="d"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<local:MainWindowViewModel />
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="30"/>
</Grid.RowDefinitions>
<DataGrid ItemsSource="{Binding DataItems}" AutoGenerateColumns="True">
<i:Interaction.Triggers>
<local:RoutedEventTrigger RoutedEvent="ScrollViewer.ScrollChanged">
<local:CustomCommandAction Command="{Binding ScrollCommand}" />
</local:RoutedEventTrigger>
</i:Interaction.Triggers>
</DataGrid>
<TextBlock Grid.Row="1" Text="{Binding ScrollData}" />
</Grid>
</Window>
ViewModel & stuff:
public class MainWindowViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
ObservableCollection<DataItem> _dataItems = new ObservableCollection<DataItem>();
public ObservableCollection<DataItem> DataItems { get { return _dataItems; } }
private TestCommand _scrollCommand;
public ICommand ScrollCommand { get { return _scrollCommand; } }
public string ScrollData { get; set; }
public MainWindowViewModel()
{
for (int i = 0; i < 100; i++)
{
_dataItems.Add(new DataItem() { Field1 = i.ToString(), Field2 = (i * 2).ToString(), Field3 = (i * 3).ToString() });
}
_scrollCommand = new TestCommand(OnScroll);
}
private void OnScroll(object param)
{
ScrollChangedEventArgs args = param as ScrollChangedEventArgs;
if (args != null)
{
ScrollData = $"VerticalChange = {args.VerticalChange}; VerticalOffset = {args.VerticalOffset}";
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ScrollData)));
}
}
}
public class DataItem
{
public string Field1 { get; set; }
public string Field2 { get; set; }
public string Field3 { get; set; }
}
public class TestCommand : ICommand
{
private Action<object> _execute;
public event EventHandler CanExecuteChanged;
public TestCommand(Action<object> execute)
{
_execute = execute;
}
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
_execute(parameter);
}
}
RoutedEventTrigger from the article:
public class RoutedEventTrigger : EventTriggerBase<DependencyObject>
{
RoutedEvent _routedEvent;
public RoutedEvent RoutedEvent
{
get { return _routedEvent; }
set { _routedEvent = value; }
}
public RoutedEventTrigger()
{
}
protected override void OnAttached()
{
Behavior behavior = base.AssociatedObject as Behavior;
FrameworkElement associatedElement = base.AssociatedObject as FrameworkElement;
if (behavior != null)
{
associatedElement = ((IAttachedObject)behavior).AssociatedObject as FrameworkElement;
}
if (associatedElement == null)
{
throw new ArgumentException("Routed Event trigger can only be associated to framework elements");
}
if (RoutedEvent != null)
{
associatedElement.AddHandler(RoutedEvent, new RoutedEventHandler(this.OnRoutedEvent));
}
}
void OnRoutedEvent(object sender, RoutedEventArgs args)
{
base.OnEvent(args);
}
protected override string GetEventName()
{
return RoutedEvent.Name;
}
}
CustomCommandAction class for passing args to the command
public sealed class CustomCommandAction : TriggerAction<DependencyObject>
{
public static readonly DependencyProperty CommandParameterProperty =
DependencyProperty.Register("CommandParameter", typeof(object), typeof(CustomCommandAction), null);
public static readonly DependencyProperty CommandProperty = DependencyProperty.Register(
"Command", typeof(ICommand), typeof(CustomCommandAction), null);
public ICommand Command
{
get
{
return (ICommand)this.GetValue(CommandProperty);
}
set
{
this.SetValue(CommandProperty, value);
}
}
public object CommandParameter
{
get
{
return this.GetValue(CommandParameterProperty);
}
set
{
this.SetValue(CommandParameterProperty, value);
}
}
protected override void Invoke(object parameter)
{
if (this.AssociatedObject != null)
{
ICommand command = this.Command;
if (command != null)
{
if (this.CommandParameter != null)
{
if (command.CanExecute(this.CommandParameter))
{
command.Execute(this.CommandParameter);
}
}
else
{
if (command.CanExecute(parameter))
{
command.Execute(parameter);
}
}
}
}
}
}
I don't know mvvmjaco but I have some hint for 2nd question. You cann't add handler to ScrollChanged directly from DataGrid. You can extend DataGrid and add custom event there. For example:
public class ExtendedDataGrid : DataGrid
{
public event ScrollChangedEventHandler ScrollChanged;
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
var scrollViewer = (ScrollViewer)GetTemplateChild("DG_ScrollViewer");
scrollViewer.ScrollChanged += OnScrollChanged;
}
protected virtual void OnScrollChanged(ScrollChangedEventArgs e)
{
ScrollChangedEventHandler handler = ScrollChanged;
if (handler != null)
{
handler(this, e);
}
}
private void OnScrollChanged(object sender, ScrollChangedEventArgs e)
{
OnScrollChanged(e);
}
}
XAML:
<local:ExtendedDataGrid>
<i:Interaction.Triggers>
<i:EventTrigger EventName="ScrollChanged">
<ei:CallMethodAction TargetObject="{Binding}"
MethodName="OnScrollChanged" />
</i:EventTrigger>
</i:Interaction.Triggers>
</local:ExtendedDataGrid>
handler:
public void OnScrollChanged(object sender, ScrollChangedEventArgs e)
{
}

How to use Key.Down to navigate items across ListViews

My scenario as below:
<ListView ItemsSource={Binding TestList}>
<ListView.ItemTemplate>
<DataTemplate>
<Expander Header="{Binding Key, BindingMode=OneWay}">
<Expander.Content>
<ListView ItemsSource={Binding Value}>
<ListView.ItemContainerStyle>
<Style TargetType = {ListViewItem}>
<Setter Property = "IsSelected" Value={Binding IsSelected}/>
</Style>
</ListView.ItemContainerStyle>
</ListView>
</Expander.Content>
</Expander>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
TestList:
Dictionary<string,ObservableCollection<Test>> TestList;
Test:
class Test : NotificationObject
{
public string Name { get; set;}
private bool isSelected;
public bool IsSelected
{
if( value != isSelected )
{
isSelected = value;
RaisePropertyChanged("IsSelected");
}
}
}
The inner ListView SelectionMode is Single, I can use keyboard up and down buttons to navigate items in the ListView. Is there a way to implement this scenario: When the focus is on the last item of a ListView, press down key, the focus is on the first item of the next ListView; when the focus is on the first item of a ListView, press up key, the focus is on the last item of the previous ListView.
Anyone can help?
Solution
public static List<T> FindVisualChildren<T>(DependencyObject depObj) where T : DependencyObject
{
List<T> list = new List<T>();
if (depObj != null)
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
{
DependencyObject child = VisualTreeHelper.GetChild(depObj, i);
if (child != null && child is T)
{
list.Add((T)child);
}
List<T> childItems = FindVisualChildren<T>(child);
if (childItems != null && childItems.Count() > 0)
{
foreach (var item in childItems)
{
list.Add(item);
}
}
}
}
return list;
}
private void ListView_PreviewKeyDown(object sender, KeyEventArgs e)
{
ListView currentListView = sender as ListView;
List<ListView> listviews = FindVisualChildren<ListView>(ModulesListView);
int currentListViewIndex = listviews.IndexOf(currentListView);
// If the press key is Down and the selected item is the last one
if (Keyboard.IsKeyDown(Key.Down)&& currentListView.SelectedIndex + 1 == currentListView.Items.Count)
{
if (currentListViewIndex + 1 < listviews.Count)
{
// Get next ListView
var nextListView = listviews.ElementAt(currentListViewIndex + 1);
if (nextListView.Items != null && nextListView.Items.Count > 0)
{
ListViewItem item = nextListView.ItemContainerGenerator.ContainerFromIndex(0) as ListViewItem;
item.Focus();
item.IsSelected = true;
e.Handled = true;
}
}
}
else if (Keyboard.IsKeyDown(Key.Up)&& currentListView.SelectedIndex == 0)
{
if (currentListViewIndex > 0)
{
var previousListView = listviews.ElementAt(currentListViewIndex - 1);
if (previousListView.Items != null && previousListView.Items.Count > 0)
{
ListViewItem item = previousListView.ItemContainerGenerator.ContainerFromIndex(previousListView.Items.Count - 1) as ListViewItem;
item.Focus();
item.IsSelected = true;
e.Handled = true;
}
}
}
}
This problem has been solved! I have paste the solution below the question, I hope it could help other members who has the same demand, thanks!
Here you go, I tried to solve it by attached properties
using attached you can implement attachable behavior which are difficult to implement in xaml
here is what you need
xaml
<Grid xmlns:l="clr-namespace:CSharpWPF">
<ListView l:NavigationHelper.IsEnabled="True">
<sys:String>item 1</sys:String>
<sys:String>item 2</sys:String>
<sys:String>item 3</sys:String>
<sys:String>item 4</sys:String>
<ListView.ItemTemplate>
<DataTemplate>
<Expander Header="{Binding}">
<Expander.Content>
<ListView ItemsSource="{Binding}"
l:NavigationHelper.IsEnabled="True">
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="IsSelected"
Value="{Binding IsSelected}" />
</Style>
</ListView.ItemContainerStyle>
</ListView>
</Expander.Content>
</Expander>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
I have attached a property l:NavigationHelper.IsEnabled="True" to my list views in order to enable the behavior
NavigationHelper.cs
namespace CSharpWPF
{
public class NavigationHelper : DependencyObject
{
public static bool GetIsEnabled(DependencyObject obj)
{
return (bool)obj.GetValue(IsEnabledProperty);
}
public static void SetIsEnabled(DependencyObject obj, bool value)
{
obj.SetValue(IsEnabledProperty, value);
}
// Using a DependencyProperty as the backing store for IsEnabled. This enables animation, styling, binding, etc...
public static readonly DependencyProperty IsEnabledProperty =
DependencyProperty.RegisterAttached("IsEnabled", typeof(bool), typeof(NavigationHelper), new PropertyMetadata(false, OnIsEnabledChanged));
private static void OnIsEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
Selector selector = d as Selector;
if ((bool)e.NewValue)
selector.PreviewKeyDown += selector_PreviewKeyDown;
else
selector.PreviewKeyDown -= selector_PreviewKeyDown;
}
static void selector_PreviewKeyDown(object sender, KeyEventArgs e)
{
Selector selector = sender as Selector;
if (selector.Items.Count == 0)
return;
ListViewItem itemToSelect = null;
if (e.Key == Key.Up && selector.SelectedIndex == 0)
{
itemToSelect = selector.ItemContainerGenerator.ContainerFromIndex(selector.Items.Count - 1) as ListViewItem;
}
else if (e.Key == Key.Down && selector.SelectedIndex == selector.Items.Count - 1)
{
itemToSelect = selector.ItemContainerGenerator.ContainerFromIndex(0) as ListViewItem;
}
if (itemToSelect != null)
{
selector.SelectedItem = itemToSelect;
itemToSelect.Focus();
e.Handled = true;
}
}
}
}
in this class I have created an attached property and attached PreviewKeyDown when enabled
and in the event handler I am checking if the up key is pressed and the selection is on top then choose the last item and select it and move focus
same with the bottom most item and key is down and choose the first item in this case.
and most important mark the event consumed by setting e.Handled = true so listview does not process it further.

Accessing checkbox inside a list box in WPF

My code is as below.
<ListBox x:Name="lstBoxMarket" BorderThickness="0" Height="Auto" HorizontalAlignment="Center" Width="200" Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2">
<ListBox.ItemTemplate>
<HierarchicalDataTemplate>
<CheckBox IsChecked="{Binding Checked}" CommandParameter="{Binding MarketId}" Tag="{Binding MarketId}" Content="{Binding Market}" Foreground="#FF3D66BE" Name="chkMarket"/>
</HierarchicalDataTemplate>
</ListBox.ItemTemplate>
</ListBox>
I want to access the selected and deselected checkboxes in the list on click of save button . I am unable to access chkMarket straight away. Can anyone help?
Starting from your code I tried something like that
// find all T in the VisualTree
public static IEnumerable<T> FindVisualChildren<T>(DependencyObject parent)
where T : DependencyObject
{
List<T> foundChilds = new List<T>();
int childrenCount = VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < childrenCount; i++)
{
var child = VisualTreeHelper.GetChild(parent, i);
T childType = child as T;
if (childType == null)
{
foreach(var other in FindVisualChildren<T>(child))
yield return other;
}
else
{
yield return (T)child;
}
}
}
Then in your MainWindow
private void button1_Click(object sender, RoutedEventArgs e)
{
// find all checkboxes in my window
IEnumerable<CheckBox> myBoxes = FindVisualChildren<CheckBox>(this);
int numChecked = 0;
foreach(CheckBox cb in myBoxes)
{
if(cb.Name != "chkMarket")
continue;
if (cb.IsChecked == true)
numChecked++;
}
MessageBox.Show("Checked items = " + numChecked);
}
My viewmodel code is
public class ViewModel
{
public ViewModel()
{
_persons = new ObservableCollection<Person>();
_persons.Add(new Person() { Name = "Paul", Checked = false });
_persons.Add(new Person() { Name = "Brian", Checked = true });
}
private ObservableCollection<Person> _persons;
public ObservableCollection<Person> Persons
{
get { return _persons; }
}
}
public class Person
{
public String Name { get; set; }
public Boolean Checked { get; set; }
}
You should be able to see the message "Checked items=1".
Hope this helps
Since it was 2 way binding i could access the values selected by the checkboxes from the item source of listbox.
DataTable lstBoxMarketItemSourceDT = ((DataView)lstBoxMarket.ItemsSource).ToTable();
"Checked" column in the data table retrieved gives the updated check box values.

wpf treeview blues. I want to select an item

I'm trying to select a TreeViewItem. Now, I have access to the containing TreeViewItem and have told it to expand so I can select its kid. If it's already expanded all is well, if it's not then I run this code:
EventHandler selector = new EventHandler(delegate
{
if (selectedDirectoryTreeItem.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
{
TreeViewItem want = selectedDirectoryTreeItem.ItemContainerGenerator.ContainerFromItem(dirWeWantSelected) as TreeViewItem;
if (want == null)
return;
want.IsSelected = true;
// selectedDirectoryTreeItem.ItemContainerGenerator.StatusChanged -= selector;
}
});
selectedDirectoryTreeItem.ItemContainerGenerator.StatusChanged += selector;
So my question is, why wont it select? want is always null. I'm scouring the interwebs looking for another way of doing this but it would be cool if somebody could explain this to me
I've personally always found it easiest to stick a Selected property into my model object and then just bind the TreeViewItem Selected property to the Selected property of the model. Here is some code:
Model
public class Data : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public Data()
{
DataItems = new List<Data>();
}
public string Name { get; set; }
private bool _selected;
public bool Selected
{
get { return _selected; }
set
{
_selected = value;
OnPropertyChanged("Selected");
}
}
public List<Data> DataItems { get; set; }
private void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
XAML
<Window x:Class="WpfApplication1.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication1"
xmlns:controls="clr-namespace:MyControls;assembly=MyControls"
Title="Window1">
<Window.Resources>
<Style x:Key="CustomTreeViewItem" TargetType="TreeViewItem">
<Setter Property="IsSelected" Value="{Binding Path=Selected, Mode=TwoWay}" />
<Setter Property="IsExpanded" Value="True" />
</Style>
</Window.Resources>
<DockPanel Background="Transparent">
<TreeView x:Name="_tvTest" DockPanel.Dock="Left" ItemContainerStyle="{StaticResource CustomTreeViewItem}" Width="300" >
<TreeView.ItemTemplate>
<HierarchicalDataTemplate DataType="{x:Type local:Data}" ItemsSource="{Binding DataItems}">
<TextBlock Text="{Binding Name}" Padding="2" />
<HierarchicalDataTemplate.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" Padding="2" />
</DataTemplate>
</HierarchicalDataTemplate.ItemTemplate>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
<Button Content="Select Random TreeView Item" Click="Button_Click" Height="50" Width="200" />
</DockPanel>
</Window>
Code behind
public partial class Window1 : Window
{
private Random _random;
private List<Data> _dataItems;
public Window1()
{
InitializeComponent();
_dataItems = Init();
_tvTest.ItemsSource = _dataItems;
_random = new Random(5);
}
private List<Data> Init()
{
List<Data> dataItems = new List<Data>();
for (int i = 1; i <= 10; i++)
{
Data d1 = new Data();
d1.Name = "Data:" + i.ToString();
for (int j = 1; j <= 4; j++)
{
Data d2 = new Data();
d2.Name = "Data:" + i.ToString() + j.ToString();
d1.DataItems.Add(d2);
}
dataItems.Add(d1);
}
return dataItems;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
int index = _random.Next(0, 9);
int subIndex = _random.Next(0, 3);
if (subIndex == 0)
_dataItems[index].Selected = true;
else
_dataItems[index].DataItems[subIndex - 1].Selected = true;
}
}
In my opinion, this is a bug in WPF, but I have run into the same issue several times. I never trust the ItemContainerGenerator's Status and instead loop on separate thread as such:
private void _selectTreeViewHelper(object directory) {
TreeViewItem want = null;
bool broke = false; //probably some sort of wait timeout instead, but works for sake of example
while (true) {
Dispatcher.Invoke(new Action(delegate {
want = selectedDirectoryTreeItem.ItemContainerGenerator.ContainerFromItem(directory) as TreeViewItem;
if (want != null && want.IsLoaded) {
want.IsSelected = true;
broke = true;
}
}));
if (broke) { break; }
Thread.Sleep(100);
}
}
Then call it:
var thread = new Thread(new ParameterizedThreadStart(_selectTreeViewHelper));
thread.Start(dirWeWantSelected);
Pain I know, but it works.
Thanks for the help, I figured it out. Well it works anyway but i'm not entirely sure why it didn't before... if anyone can tell me it would be lovely... I kinda combined elements of the two responses above and then added on a little bit to make it work. For some reason, no matter what i do I cant select the TVI (TreeViewElement) if its parent wasn't already expanded, even if the parent TVI said its contents were generated and, in fact, had contents i could iterate through. My solution was thus to wait for the contents to be generated and then itrate through them and find the one i wanted. It's really weird to me that I couldn't just grab a container given its contents. Meh. My code could stand to be refactored a little bit but here it is: (works perfectly)
public void listItemClickClick(object sender, RoutedEventArgs e)
{
try
{
UserFile fil = (UserFile)(sender as ListBoxItem).DataContext;
MessageBox.Show("to do: download stuff");
return;
}
catch (InvalidCastException)
{
}
try
{
dirWeWantSelected = (Directory)(sender as ListBoxItem).DataContext;
}
catch (InvalidCastException)
{
MessageBox.Show("this should never happen");
}
selectedDirectoryTreeItem.IsExpanded = true;
TreeViewItem want = null;
try
{
want = selectedDirectoryTreeItem.ItemContainerGenerator.ContainerFromItem(dirWeWantSelected) as TreeViewItem;
}
catch
{
MessageBox.Show("weird error");
}
if (want != null)
{
want.IsSelected = true;
}
else
{
selectedDirectoryTreeItem.ItemContainerGenerator.StatusChanged += ItemContainerGenerator_StatusChanged;
}
}
void ItemContainerGenerator_StatusChanged(object sender, EventArgs e)
{
if (selectedDirectoryTreeItem.ItemContainerGenerator.Status
== System.Windows.Controls.Primitives.GeneratorStatus.ContainersGenerated)
{
selectedDirectoryTreeItem.ItemContainerGenerator.StatusChanged
-= ItemContainerGenerator_StatusChanged;
Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Input,
new Action(DelayedAction));
}
}
void DelayedAction()
{
selectedDirectoryTreeItem.Items.MoveCurrentToFirst();
Directory curr;
do
{
curr = (Directory)selectedDirectoryTreeItem.Items.CurrentItem;
if (curr.id == dirWeWantSelected.id)
{
curr.Selected = true;
return;
}
selectedDirectoryTreeItem.Items.MoveCurrentToNext();
}
while (selectedDirectoryTreeItem.Items.CurrentItem != null);
}

Categories