WPF, treeview is mouseover only works after first time - c#

I have a treeview where I have added an eventsetter to the item container style to catch whenever a F1 is pressed while hovering a mouse over. So in the code behind I tried to find the child object that the mouse is over. The child object is only found in the tree after a node have been expanded and tried once before, the key down is catched correctly every time. So it is only the second time the IsMouseOver child object is found.
I have disabled virtualization for the target tree, but it doesn't make any difference.
<HierarchicalDataTemplate.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<EventSetter Event="PreviewKeyDown" Handler="EventSetter_OnHandler"></EventSetter>
<Setter Property="IsSelected">
<Setter.Value>
<MultiBinding Mode="OneWay" Converter="{StaticResource ActiveReportTypeMatchToBoolConverter}">
<Binding Path="DataContext.ActiveReportType" ElementName="TreeViewExpander" />
<Binding />
</MultiBinding>
</Setter.Value>
</Setter>
<Setter Property="UIElement.Uid" Value="{Binding Name}" />
</Style>
</HierarchicalDataTemplate.ItemContainerStyle>
The Code behind event handler
private void EventSetter_OnHandler(object sender, KeyEventArgs e) {
if (e.Key == Key.F1) {
foreach (var item in TreeViewReportType.Items) {
TreeViewItem anItem = TreeViewReportType.ItemContainerGenerator.ContainerFromItem(item) as TreeViewItem;
if (anItem?.IsMouseOver == true) {
foreach (ReportType childItem in anItem.Items) {
TreeViewItem childTreeViewItem = anItem.ItemContainerGenerator.ContainerFromItem(childItem) as TreeViewItem;
if (childTreeViewItem?.IsMouseOver == true) {
ApplicationCommands.Help.Execute(childItem.HelpId, childTreeViewItem);
}
}
return;
}
}
}
}
Do any of you know of a magic trick here ?
I have tried to do a TreeViewReportType.UpdateLayout() and also anItem.UpdateLayout() to see if it made any changes. But it didn't help.
Tried to look on previous answers but it is datagrid related and is to disable virtualization, which doesn't work here ?

Controls, such as TreeViewItems, have to have focus in order to receive keyboard events.
You've got a few options here. You can put the event on the TreeView itself, but that only works if the treeview has keyboard focus. As long as it's the only control in the window, you're fine. If it isn't, you're in trouble and you need to handle the key event at the window level. Another option would be to write a trigger in your ItemContainerStyle which gives the tree view items keyboard focus when IsMouseOver. But that's silly.
Let's do it at the window level, because we can generalize it: Wherever you press F1 in this window, we'll search the control under the mouse and its parents for one that has a DataContext with a HelpId property. If we find one, we'll use it. If ReportType is the only vm class that has a HelpId, it'll work fine. If you add HelpId to another class, that'll just work. If you decide to list ReportTypes in a ListView too, that'll just work.
This replaces all of the code in your question.
MainWindow.xaml
...
PreviewKeyDown="Window_PreviewKeyDown"
...
MainWindow.xaml.cs
private void Window_PreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.F1)
{
var window = sender as Window;
var point = Mouse.GetPosition(window);
Helpers.ExecuteHelpUnderPoint(window, point);
}
}
Helpers.cs
public static class Helpers
{
public static void ExecuteHelpUnderPoint(FrameworkElement parent, Point point)
{
var hittestctl = parent.InputHitTest(point) as FrameworkElement;
var helpID = GetNearestDataContextHelpID(hittestctl);
if (helpID != null)
{
ApplicationCommands.Help.Execute(helpID, hittestctl);
}
}
public static IEnumerable<T> GetAncestorsOfType<T>(DependencyObject dobj) where T : DependencyObject
{
dobj = VisualTreeHelper.GetParent(dobj);
while (dobj != null)
{
if (dobj is T t)
yield return t;
dobj = VisualTreeHelper.GetParent(dobj);
}
}
public static Object GetNearestDataContextHelpID(DependencyObject dobj)
{
var dataContexts = GetAncestorsOfType<FrameworkElement>(dobj)
.Select(fe => fe.DataContext).Where(dc => dc != null);
// LINQ distinct probably doesn't affect order, but that's not guaranteed.
// https://stackoverflow.com/a/4734876/424129
object prev = null;
foreach (var dc in dataContexts)
{
if (dc != prev)
{
var prop = dc.GetType().GetProperty("HelpId");
if (prop != null)
{
var value = prop.GetValue(dc);
if (value != null)
{
return value;
}
}
prev = dc;
}
}
return null;
}
}

Related

WPF drag-drop rearrangeable wrappanel items

I need something that arranges/sorts items like a wrappanel but allows you to rearrange the items via drag & drop. I'm trying to prototype this out as quick as possible, so I'm just looking for something stupidly simple or hacky for now. I've been looking all over the place, but the only answers I could find required a relatively large amount of work to implement.
I would prefer not using an existing library if it's possible.
EDIT
To clarify: I need drag & drop as well as auto-rearrange mechanics within a control that arranges items like a wrappanel.
Since posting this question, I've found two posts/articles that seemed to be a good fit but only if combined.
Wrappanel: http://www.codeproject.com/Articles/18561/Custom-ListBox-Layout-in-WPF
Drag-drop & arrangement: WPF C#: Rearrange items in listbox via drag and drop
I'll post my code in an answer when I get it all working.
It took some time, but I was able to figure it out after Micky's suggestion. I'm posting an answer here for future reference and if anyone else is looking for this mechanic. The following code snippets should work by just pasting them into their appropriate files.
Here's what worked:
Make the ListBox arrange items like a WrapPanel via a custom/default style (mine is called Default.xaml).
<Style TargetType="{x:Type ListBox}">
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<!--'WrapPanel' can be replaced with other controls if you want it to display differently-->
<WrapPanel/>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled"/>
<Setter Property="ItemTemplate">
<Setter.Value>
<DataTemplate>
<!--Controls in each item-->
<local:DocPage />
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
Set up the ListBox control (i.e. MainWindow.xaml):
<ListBox x:Name="lbx_Pages" AllowDrop="True" DragEnter="lbx_Pages_DragEnter" PreviewMouseLeftButtonDown="lbx_Pages_PreviewMouseLeftButtonDown" PreviewMouseMove="lbx_Pages_PreviewMouseMove" Drop="lbx_Pages_PagesDrop"/>
Impliment the controls in the .cs file (i.e. MainWindow.xaml.cs):
private Point dragStartPoint;
// Bindable list of pages (binding logic omitted-out of scope of this post)
private static ObservableCollection<DocPage> pages = new ObservableCollection<DocPage>();
// Find parent of 'child' of type 'T'
public static T FindParent<T>(DependencyObject child) where T : DependencyObject
{
do
{
if (child is T)
return (T)child;
child = VisualTreeHelper.GetParent(child);
} while (child != null);
return null;
}
private void lbx_Pages_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
dragStartPoint = e.GetPosition(null);
}
private void lbx_Pages_PreviewMouseMove(object sender, MouseEventArgs e)
{
ListBoxItem item = null;
DataObject dragData;
ListBox listBox;
DocPage page;
// Is LMB down and did the mouse move far enough to register a drag?
if (e.LeftButton == MouseButtonState.Pressed &&
(Math.Abs(dragStartPoint.X - e.GetPosition(null).X) > SystemParameters.MinimumHorizontalDragDistance ||
Math.Abs(dragStartPoint.Y - e.GetPosition(null).Y) > SystemParameters.MinimumVerticalDragDistance))
{
// Get the ListBoxItem object from the object being dragged
item = FindParent<ListBoxItem>((DependencyObject)e.OriginalSource);
if (null != item)
{
listBox = sender as ListBox;
page = (DocPage)listBox.ItemContainerGenerator.ItemFromContainer(item);
dragData = new DataObject("pages", page);
DragDrop.DoDragDrop(item, dragData, DragDropEffects.Move);
}
}
}
private void lbx_Pages_PagesDrop(object sender, DragEventArgs e)
{
if (!e.Data.GetDataPresent("pages"))
return;
DocPage draggedItem = e.Data.GetData("pages") as DocPage;
// Hit-test needed for rearranging items in the same ListBox
HitTestResult hit = VisualTreeHelper.HitTest((ListBox)sender, e.GetPosition((ListBox)sender));
DocPage target = (DocPage)FindParent<ListBoxItem>(hit.VisualHit).DataContext;
int removeIdx = lbx_Pages.Items.IndexOf(draggedItem);
int targetIdx = lbx_Pages.Items.IndexOf(target);
if(removeIdx < targetIdx)
{
pages.Insert(targetIdx + 1, draggedItem);
pages.RemoveAt(removeIdx);
}
else
{
removeIdx++;
if(pages.Count+1 > removeIdx)
{
pages.Insert(targetIdx, draggedItem);
pages.RemoveAt(removeIdx);
}
}
}
private void lbx_Pages_DragEnter(object sender, DragEventArgs e)
{
if (!e.Data.GetDataPresent("pages") || sender == e.Source)
e.Effects = DragDropEffects.None;
}

Get parent TreeViewItem of a selected node in WPF

I want the parent of a node that is selected as TreeViewItem
I have a Person class with 2 fields. Name(String) and Children(List of string)
This is my xaml code
<Grid x:Name="gridView" Margin="10">
<TreeView Name="treeView1" TreeViewItem.Selected="TreeViewItem_OnItemSelected" ItemsSource="{Binding}">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Source=Check, Mode=TwoWay}" />
</Style>
</TreeView.ItemContainerStyle>
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type loc:Person}" ItemsSource="{Binding Children}" >
<TextBlock Text="{Binding Name}" />
</HierarchicalDataTemplate>
</TreeView.Resources>
</TreeView>
</Grid>
this is my code behind.
I set the item source to a list of Person objects.
void set()
{
if (treeView1.Items.IndexOf(treeView1.SelectedItem) != -1)
{
//is a parent
//returns -1 for children
Person selected = (Person)treeView1.SelectedItem;
int index = search(selected);
TreeViewItem parent = treeView1.Tag as TreeViewItem;
setSelected(parent,index);
}
else
{
//is a child
TreeViewItem child = treeView1.Tag as TreeViewItem; //returns the selected node
TreeViewItem parent = child.Parent as TreeViewItem; //returns null
}
}
private void TreeViewItem_OnItemSelected(object sender, RoutedEventArgs e)
{
treeView1.Tag = e.OriginalSource;
int ind = 0;
foreach (var _item in treeView1.Items)
{
if (_item == treeView1.SelectedItem)
{
selectedIndex = ind;
break;
}
ind++;
}
}
In the else part, The child.Parent always returns null. I tried other methods but none of them return a TreeViewItem instead they return DependencyObject or ItemsControl.
I also tried ContainerFromItem method but it only works for direct children(parent) and not the children of the parent.
Please help
You could use the VisualTreeHelper.GetParent() method:
https://msdn.microsoft.com/de-de/library/system.windows.media.visualtreehelper.getparent(v=vs.110).aspx
Example Code:
private TreeViewItem FindParentTreeViewItem(object child)
{
try
{
var parent = VisualTreeHelper.GetParent(child as DependencyObject);
while ((parent as TreeViewItem) == null)
{
parent = VisualTreeHelper.GetParent(parent);
}
return parent as TreeViewItem;
}
catch (Exception e)
{
//could not find a parent of type TreeViewItem
return null;
}
}
The while loop is needed, as the visual parent of a tree view item isn't its parent tree view item as can be seen in this WPF Tree Visualizer:
WPF Tree Visualizer showing tree view
ItemsSource is typically a collection of DependencyObjects which you can bind to. It doesn't reflect any information about UI. What you are looking for is actually the ItemContainer of each Item which is a part of WPF UI.
In most cases you can access a UIElement from another without any problem. But in my experience as the UI gets complicated by Styles, Templates and hierarchical items, some UIElements will be hard to find.
I suggest the following way:
Implement a Parent property in your ViewModel (Person Class) with type of Person and initialize it in the constructor of Person, Then you can get both the parent Person and the parent TreeViewItem like this:
var parentPerson = (treeView1.SelectedItem as Person).Parent;
var parentNode = treeView1.ItemContainerGenerator.ContainerFromItem(parentPerson);

WPF UI thread freeze on loading bunch of items in TreeView

In a past few months I've played a lot with the TreeView and now I get to the UI freeze problem. It comes when you have large amount of the items and the data part for those Items are created very quickly but creating TreeViewItems and visualizing those (it must be done on UI thread) takes a time.
Let's take Shell browser and C:\Windows\System32 directory as an example. (I reworked http://www.codeproject.com/Articles/24237/A-Multi-Threaded-WPF-TreeView-Explorer solution for that.) This directory has ~2500 files and folders.
The DataItem and Visual loading are implemented in different threads but as the file and directory info are read quickly it gives no benefit. Application freezes when it creates TreeViewItems and makes those visible.
I've tried:
Set a different DispatcherPriorities for the UI thread when to load items, for example the window was interactive (I was able to move it) with DispatcherPriority.ContextIdle, but then items were loaded really slow..
Create and visualize items in blocks, like 100 items per once, but hat no benefit, the UI thread still were freezing..
My goal is that the application would be interactive while loading those item's!
At the moment I have only one idea how to solve this, to implement my own control which tracks window size, scrollbar position and loads only the items which are visable, but it's not so easy to do that and I'm not sure that at the end performance would be better.. :)
Maybe somebody has idea how to make application interactive while loading bunch of visual items?!
Code:
Complete Solution could be found there: http://www.speedyshare.com/hksN6/ShellBrowser.zip
Program:
public partial class DemoWindow
{
public DemoWindow()
{
InitializeComponent();
this.Loaded += DemoWindow_Loaded;
}
private readonly object _dummyNode = null;
delegate void LoaderDelegate(TreeViewItem tviLoad, string strPath, DEL_GetItems actGetItems, AddSubItemDelegate actAddSubItem);
delegate void AddSubItemDelegate(TreeViewItem tviParent, IEnumerable<ItemToAdd> itemsToAdd);
// Gets an IEnumerable for the items to load, in this sample it's either "GetFolders" or "GetDrives"
// RUNS ON: Background Thread
delegate IEnumerable<ItemToAdd> DEL_GetItems(string strParent);
void DemoWindow_Loaded(object sender, RoutedEventArgs e)
{
var tviRoot = new TreeViewItem();
tviRoot.Header = "My Computer";
tviRoot.Items.Add(_dummyNode);
tviRoot.Expanded += OnRootExpanded;
tviRoot.Collapsed += OnItemCollapsed;
TreeViewItemProps.SetItemImageName(tviRoot, #"Images/Computer.png");
foldersTree.Items.Add(tviRoot);
}
void OnRootExpanded(object sender, RoutedEventArgs e)
{
var treeViewItem = e.OriginalSource as TreeViewItem;
StartItemLoading(treeViewItem, GetDrives, AddItem);
}
void OnItemCollapsed(object sender, RoutedEventArgs e)
{
var treeViewItem = e.OriginalSource as TreeViewItem;
if (treeViewItem != null)
{
treeViewItem.Items.Clear();
treeViewItem.Items.Add(_dummyNode);
}
}
void OnFolderExpanded(object sender, RoutedEventArgs e)
{
var tviSender = e.OriginalSource as TreeViewItem;
e.Handled = true;
StartItemLoading(tviSender, GetFilesAndFolders, AddItem);
}
void StartItemLoading(TreeViewItem tviSender, DEL_GetItems actGetItems, AddSubItemDelegate actAddSubItem)
{
tviSender.Items.Clear();
LoaderDelegate actLoad = LoadSubItems;
actLoad.BeginInvoke(tviSender, tviSender.Tag as string, actGetItems, actAddSubItem, ProcessAsyncCallback, actLoad);
}
void LoadSubItems(TreeViewItem tviParent, string strPath, DEL_GetItems actGetItems, AddSubItemDelegate actAddSubItem)
{
var itemsList = actGetItems(strPath).ToList();
Dispatcher.BeginInvoke(DispatcherPriority.Normal, actAddSubItem, tviParent, itemsList);
}
// Runs on Background thread.
IEnumerable<ItemToAdd> GetFilesAndFolders(string strParent)
{
var list = Directory.GetDirectories(strParent).Select(itemName => new ItemToAdd() {Path = itemName, TypeOfTheItem = ItemType.Directory}).ToList();
list.AddRange(Directory.GetFiles(strParent).Select(itemName => new ItemToAdd() {Path = itemName, TypeOfTheItem = ItemType.File}));
return list;
}
// Runs on Background thread.
IEnumerable<ItemToAdd> GetDrives(string strParent)
{
return (Directory.GetLogicalDrives().Select(x => new ItemToAdd(){Path = x, TypeOfTheItem = ItemType.DiscDrive}));
}
void AddItem(TreeViewItem tviParent, IEnumerable<ItemToAdd> itemsToAdd)
{
string imgPath = "";
foreach (ItemToAdd itemToAdd in itemsToAdd)
{
switch (itemToAdd.TypeOfTheItem)
{
case ItemType.File:
imgPath = #"Images/File.png";
break;
case ItemType.Directory:
imgPath = #"Images/Folder.png";
break;
case ItemType.DiscDrive:
imgPath = #"Images/DiskDrive.png";
break;
}
if (itemToAdd.TypeOfTheItem == ItemType.Directory || itemToAdd.TypeOfTheItem == ItemType.File)
IntAddItem(tviParent, System.IO.Path.GetFileName(itemToAdd.Path), itemToAdd.Path, imgPath);
else
IntAddItem(tviParent, itemToAdd.Path, itemToAdd.Path, imgPath);
}
}
private void IntAddItem(TreeViewItem tviParent, string strName, string strTag, string strImageName)
{
var tviSubItem = new TreeViewItem();
tviSubItem.Header = strName;
tviSubItem.Tag = strTag;
tviSubItem.Items.Add(_dummyNode);
tviSubItem.Expanded += OnFolderExpanded;
tviSubItem.Collapsed += OnItemCollapsed;
TreeViewItemProps.SetItemImageName(tviSubItem, strImageName);
tviParent.Items.Add(tviSubItem);
}
private void ProcessAsyncCallback(IAsyncResult iAR)
{
// Call end invoke on UI thread to process any exceptions, etc.
Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal, (Action)(() => ProcessEndInvoke(iAR)));
}
private void ProcessEndInvoke(IAsyncResult iAR)
{
try
{
var actInvoked = (LoaderDelegate)iAR.AsyncState;
actInvoked.EndInvoke(iAR);
}
catch (Exception ex)
{
// Probably should check for useful inner exceptions
MessageBox.Show(string.Format("Error in ProcessEndInvoke\r\nException: {0}", ex.Message));
}
}
private struct ItemToAdd
{
public string Path;
public ItemType TypeOfTheItem;
}
private enum ItemType
{
File,
Directory,
DiscDrive
}
}
public static class TreeViewItemProps
{
public static string GetItemImageName(DependencyObject obj)
{
return (string)obj.GetValue(ItemImageNameProperty);
}
public static void SetItemImageName(DependencyObject obj, string value)
{
obj.SetValue(ItemImageNameProperty, value);
}
public static readonly DependencyProperty ItemImageNameProperty;
static TreeViewItemProps()
{
ItemImageNameProperty = DependencyProperty.RegisterAttached("ItemImageName", typeof(string), typeof(TreeViewItemProps), new UIPropertyMetadata(string.Empty));
}
}
Xaml:
<Window x:Class="ThreadedWpfExplorer.DemoWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ThreadedWpfExplorer"
Title="Threaded WPF Explorer" Height="840" Width="350" Icon="/ThreadedWpfExplorer;component/Images/Computer.png">
<Grid>
<TreeView x:Name="foldersTree">
<TreeView.Resources>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="HeaderTemplate">
<Setter.Value>
<DataTemplate DataType="ContentPresenter">
<Grid>
<StackPanel Name="spImg" Orientation="Horizontal">
<Image Name="img"
Source="{Binding
RelativeSource={RelativeSource
Mode=FindAncestor,
AncestorType={x:Type TreeViewItem}},
Path=(local:TreeViewItemProps.ItemImageName)}"
Width="20" Height="20" Stretch="Fill" VerticalAlignment="Center" />
<TextBlock Text="{Binding}" Margin="5,0" VerticalAlignment="Center" />
</StackPanel>
</Grid>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</TreeView.Resources>
</TreeView>
</Grid>
</Window>
Alternative Loading items in blocks:
private const int rangeToAdd = 100;
void LoadSubItems(TreeViewItem tviParent, string strPath, DEL_GetItems actGetItems, AddSubItemDelegate actAddSubItem)
{
var itemsList = actGetItems(strPath).ToList();
int index;
for (index = 0; (index + rangeToAdd) <= itemsList.Count && rangeToAdd <= itemsList.Count; index = index + rangeToAdd)
{
Dispatcher.BeginInvoke(DispatcherPriority.Normal, actAddSubItem, tviParent, itemsList.GetRange(index, rangeToAdd));
}
if (itemsList.Count < (index + rangeToAdd) || rangeToAdd > itemsList.Count)
{
var itemsLeftToAdd = itemsList.Count % rangeToAdd;
Dispatcher.BeginInvoke(DispatcherPriority.Normal, actAddSubItem, tviParent, itemsList.GetRange((rangeToAdd > itemsList.Count) ? index : index - rangeToAdd, itemsLeftToAdd));
}
}
What you're looking for is known as UI Virtualization and is supported by a number of different WPF controls. Regarding the TreeView in particular, see this article for details on how to turn on virtualization.
One major caveat is that in order to benefit from this feature, you need to use the ItemsSource property and provide items from a collection rather than adding items directly from your code. This is a good idea to do anyway, but it may require some restructuring to get it functional with your existing code.
Why not just create your observable collection and bind to it from xaml?
Check out the MvvM design pattern and you just create a class, and point the xaml at it, in there, from the initialisation, create your list, and then tell the treeview to bind to that list, displaying properties of the each item in your list.
I know this is a little scant on info, but to do MvvM is really easy and just look through stackoverflow and you'll see examples.
You really don't need to call begininvoke on every item - and that's just not from an mvvm point of view - just bind to a list.
You can use indexed 'levels' to your objects too.
Another helpful technique is this regard, is Data Virtualization. There is a good article and sample project on CodeProject, that talks about Data Virtualization in WPF.

DataGrid Select Entire Column

I am using WPF DataGrid and I would like to allow the user to select an entire column by clicking on the column header. I currently have the SelectionUnit set to CellOrRowHeader because I want the same functionality for rows (which works great). Is there an easy way to also allow selection of columns by clicking the column header?
You have many options. one is create your own Template for DataGrid's Header Style. Inside the DataTemplate tag you can change the template of the headers. (you can replace the Button with a TextBlock or anything you want.)
<DataGrid>
<DataGrid.ColumnHeaderStyle>
<Style TargetType="DataGridColumnHeader" >
<Setter Property="Foreground" Value="Blue" />
<Setter Property="ContentTemplate" >
<Setter.Value>
<DataTemplate>
<Button Content={Binding Content}" MouseDown="mouseDownEventHandler">
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</DataGrid.ColumnHeaderStyle>
</DataGrid>
or if you are using DataGrid.Columns to populate your columns and you need to set each of them separately, you can use this:
<DataGrid>
<DataGrid.Columns>
<DataGridTextColumn HeaderTemplate="{StaticResource MyTemplate1"/>
<DataGridHyperlinkColumn HeaderTemplate="{StaticResource MyTemplate2"/>
</DataGrid.Columns>
</DatGrid>
where MyTemplate1 and 2 should be defined previously in your control's Resources.
Edit:
The other approach according to this link is to add a PreviewMouseDown to your DataGrid and then find out if the mouse down was happend on the header or not.
This is the simple version of her event handler:
DependencyObject dep = (DependencyObject)e.OriginalSource;
while ((dep != null) && !(dep is DataGridColumnHeader))
{
dep = VisualTreeHelper.GetParent(dep);
}
if (dep == null) return;
if (dep is DataGridColumnHeader)
{
MessageBox.Show(((DataGridColumnHeader)dep).Content.ToString());
}
You could also modify the ColumnHeaderStyle:
In XAML:
<DataGrid>
<DataGrid.ColumnHeaderStyle>
<Style TargetType="DataGridColumnHeader">
<EventSetter Event="Click" Handler="DataGridColumnHeader_OnClick"></EventSetter>
</Style>
</DataGrid.ColumnHeaderStyle>
</DataGrid>
In the code-behind:
private void DataGridColumnHeader_OnClick(object sender, RoutedEventArgs e)
{
var columnHeader = sender as DataGridColumnHeader;
if (columnHeader != null)
{
if (!Keyboard.IsKeyDown(Key.LeftCtrl))
{
dataGrid.SelectedCells.Clear();
}
foreach (var item in dataGrid.Items)
{
dataGrid.SelectedCells.Add(new DataGridCellInfo(item, columnHeader.Column));
}
}
}
In WPF there is no direct support for column selection so, need to Extend existing DataGrid and add custom support for Column Selection.
Below is the code that will allow used to select column on Header click.
For Below code to work following setting is required.
Xaml Code:
<local:DataGridEx SelectionUnit="CellOrRowHeader" CanUserSortColumns="False"/>
C# Code:
public class DataGridEx : DataGrid
{
/// <summary>
/// Holds the column that is selected.
/// </summary>
public object SelectedColumn
{
get { return GetValue(SelectedColumnProperty); }
set { SetValue(SelectedColumnProperty, value); }
}
// Using a DependencyProperty as the backing store for SelectedColumn. This enables animation, styling, binding, etc...
public static readonly DependencyProperty SelectedColumnProperty =
DependencyProperty.Register("SelectedColumn", typeof(object),
typeof(DataGridEx), new PropertyMetadata(null));
private T GetVisualParent<T>(DependencyObject child) where T : Visual
{
DependencyObject parent = VisualTreeHelper.GetParent(child);
if (parent == null || parent is T)
{
return parent as T;
}
else
{
return GetVisualParent<T>(parent);
}
}
protected override void OnPreviewMouseDown(MouseButtonEventArgs e)
{
DataGridColumnHeader dataGridColumnHeader = GetVisualParent<DataGridColumnHeader>(e.OriginalSource as DependencyObject);
if (dataGridColumnHeader == null)
{
return;
}
if (SelectedCells != null && SelectedCells.Count > 0)
{
UnselectAllCells();
SelectedCells.Clear();
}
SelectedColumn = dataGridColumnHeader.Column;
foreach (var item in this.Items)
{
this.SelectedCells.Add(new DataGridCellInfo(item, dataGridColumnHeader.Column));
}
base.OnPreviewMouseDown(e);
}
}

ListBox ScrollIntoView when using CollectionViewSource with GroupDescriptions (i.e. IsGrouping == True)

Short version
I would like to scroll the ListBox item into view when the selection is changed.
Long version
I have a ListBox with the ItemsSource bound to a CollectionViewSource with a GroupDescription, as per the example below.
<Window.Resources>
<CollectionViewSource x:Key="AnimalsView" Source="{Binding Source={StaticResource Animals}, Path=AnimalList}">
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="Category"/>
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>
</Window.Resources>
<ListBox x:Name="AnimalsListBox"ItemsSource="{Binding Source={StaticResource AnimalsView}}" ItemTemplate="{StaticResource AnimalTemplate}" SelectionChanged="ListBox_SelectionChanged">
<ListBox.GroupStyle>
<GroupStyle HeaderTemplate="{StaticResource CategoryTemplate}" />
</ListBox.GroupStyle>
</ListBox>
There is a SelectionChanged event in the a code-behind file.
public List<Animal> Animals { get; set; }
private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
ListBox control = (ListBox)sender;
control.ScrollIntoView(control.SelectedItem);
}
Now. If I set the AnimalsListBox.SelectedItem to an item that is currently not visible I would like to have it scroll in view. This is where it gets tricky, as the ListBox is being groups (the IsGrouped property is true) the call to ScrollIntoView fails.
System.Windows.Controls.ListBox via Reflector. Note the base.IsGrouping in the OnBringItemIntoView.
public void ScrollIntoView(object item)
{
if (base.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
{
this.OnBringItemIntoView(item);
}
else
{
base.Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new DispatcherOperationCallback(this.OnBringItemIntoView), item);
}
}
private object OnBringItemIntoView(object arg)
{
FrameworkElement element = base.ItemContainerGenerator.ContainerFromItem(arg) as FrameworkElement;
if (element != null)
{
element.BringIntoView();
}
else if (!base.IsGrouping && base.Items.Contains(arg))
{
VirtualizingPanel itemsHost = base.ItemsHost as VirtualizingPanel;
if (itemsHost != null)
{
itemsHost.BringIndexIntoView(base.Items.IndexOf(arg));
}
}
return null;
}
Questions
Can anyone explain why it does not work when using grouping?
The ItemContainerGenerator.ContainerFromItem always returns null, even though it's status states that all the containers have been generated.
How I can achieve the scrolling into view when using grouping?
I have found a solution to my problem. I was certain that I wasn't the first person to hit this issue so I continued to search StackOverflow for solutions and I stumbled upon this answer by David about how ItemContainerGenerator works with a grouped list.
David's solution was to delay accessing the ItemContainerGenerator until after the rendering process.
I have implemented this solution, with a few changes that I will detail after.
private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
ListBox control = (ListBox)sender;
if (control.IsGrouping)
{
if (control.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
Dispatcher.BeginInvoke(DispatcherPriority.Render, new Action(DelayedBringIntoView));
else
control.ItemContainerGenerator.StatusChanged += ItemContainerGenerator_StatusChanged;
}
else
control.ScrollIntoView(control.SelectedItem);
}
private void ItemContainerGenerator_StatusChanged(object sender, EventArgs e)
{
if (ItemContainerGenerator.Status != GeneratorStatus.ContainersGenerated)
return;
ItemContainerGenerator.StatusChanged -= ItemContainerGenerator_StatusChanged;
Dispatcher.BeginInvoke(DispatcherPriority.Render, new Action(DelayedBringIntoView));
}
private void DelayedBringIntoView()
{
var item = ItemContainerGenerator.ContainerFromItem(SelectedItem) as ListBoxItem;
if (item != null)
item.BringIntoView();
}
Changes:
Only uses the ItemContainerGenerator approach when it IsGrouping is true, otherwise continue to use the default ScrollIntoView.
Check if the ItemContainerGenerator is ready, if so dispatch the action, otherwise listen for the ItemContainerGenerator status to change.. This is important as if it is ready then the StatusChanged event will never fire.
The out of the box VirtualizingStackPanel does not support virtualizing grouped collection views. When a grouped collection is rendered in an ItemsControl, each group as a whole is an item as opposed to each item in the collection which results in "jerky" scrolling to each group header and not each item.
You'll probably need to roll your own VirtualizingStackPanel or ItemContainerGenerator in order to keep track of the containers displayed in a group. It sounds ridiculous, but the default virtualization with grouping in WPF is lacking to say the least.

Categories