WPF treeview: how to implement keyboard navigation like in Explorer? - wpf

I am using the WPF treeview for the first time and am astonished of all the basic things it does not do. One of those is keyboard navigation, implemented in any self-respecting treeview, e.g. in Windows Explorer or Regedit.
This is how it should work:
If the treeview has the focus and I type (letters/numbers) the selection should move to the first visible (aka expanded) item below the currently selected item that matches the string I typed and bring that into view. If not match is found below the current item the search should continue from the top. If no match is found, the selected item should not change.
As long as I continue typing, the search string grows and the search is refined. If I stop typing for a certain time (2-5 seconds), the search string is emptied.
I am prepared to program this "by hand" from scratch, but since this is so very basic I thought surely someone has already done exactly this.

Funny, this does not seem to be a popular topic. Anyway, in the meantime I have developed a solution to the problem that satisfies me:
I attach a behavior to the TreeViewItems. In that behavior, I handle KeyUp events. In the KeyUp event handler, I search the visual tree top to bottom as it is displayed. If I find a first matching node (whose name starts with the letter on the key pressed) I select that node.

I know that is an old topic, but I guess it is still relevant for some people. I made this solution. It is attached to the KeyUp and the TextInput event on a WPF TreeView. I'm using TextInput in addition to KeyUp as I had difficulty translating "national" chars to real chars with KeyEventArgs. That went much more smooth with TextInput.
// <TreeView Name="treeView1" KeyUp="treeView1_KeyUp" TextInput="treeView1_TextInput"/>
private bool searchdeep = true; // Searches in subitems
private bool searchstartfound = false; // true when current selected item is found. Ensures that you don't seach backwards and that you only search on the current level (if not searchdeep is true)
private string searchterm = ""; // what to search for
private DateTime LastSearch = DateTime.Now; // resets searchterm if last input is older than 1 second.
private void treeView1_KeyUp(object sender, System.Windows.Input.KeyEventArgs e)
{
// reset searchterm if any "special" key is pressed
if (e.Key < Key.A)
searchterm = "";
}
private void treeView1_TextInput(object sender, TextCompositionEventArgs e)
{
if ((DateTime.Now - LastSearch).Seconds > 1)
searchterm = "";
LastSearch = DateTime.Now;
searchterm += e.Text;
searchstartfound = treeView1.SelectedItem == null;
foreach (var t in treeView1.Items)
if (SearchTreeView((TreeViewItem) t, searchterm.ToLower()))
break;
}
private bool SearchTreeView(TreeViewItem node, string searchterm)
{
if (node.IsSelected)
searchstartfound = true;
// Search current level first
foreach (TreeViewItem subnode in node.Items)
{
// Search subnodes to the current node first
if (subnode.IsSelected)
{
searchstartfound = true;
if (subnode.IsExpanded)
foreach (TreeViewItem subsubnode in subnode.Items)
if (searchstartfound && subsubnode.Header.ToString().ToLower().StartsWith(searchterm))
{
subsubnode.IsSelected = true;
subsubnode.IsExpanded = true;
subsubnode.BringIntoView();
return true;
}
}
// Then search nodes on the same level
if (searchstartfound && subnode.Header.ToString().ToLower().StartsWith(searchterm))
{
subnode.IsSelected = true;
subnode.BringIntoView();
return true;
}
}
// If not found, search subnodes
foreach (TreeViewItem subnode in node.Items)
{
if (!searchstartfound || searchdeep)
if (SearchTreeView(subnode, searchterm))
{
node.IsExpanded = true;
return true;
}
}
return false;
}

I was also looking for keyboard navigation, amazing how not obvious the solution was for templated items.
Setting SelectedValuePath in ListView or TreeView gives this behavior.
If the items are templated then setting the attached property: TextSearch.TextPath to the path of the property to search on will also do the trick.
Hope this helps, it definitely worked for me.

Since this question comes up most prominently when searching, I wanted to post an answer to it.
The above post by lars doesn't work for me when I'm using a databound TreeView with a HierarchicalDataTemplate, because the Items collection returns the actual databound items, not the TreeViewItem.
I ended up solving this by using the ItemContainerGenerator for individual data items, and the VisualTreeHelper to search "up" to find the parent node (if any). I implemented this as a static helper class so that I can easily reuse it (which for me is basically every TreeView).
Here's my helper class:
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace TreeViewHelpers
{
public static class TreeViewItemTextSearcher
{
private static bool checkIfMatchesText(TreeViewItem node, string searchterm, StringComparison comparison)
{
return node.Header.ToString().StartsWith(searchterm, comparison);
}
//https://stackoverflow.com/questions/26624982/get-parent-treeviewitem-of-a-selected-node-in-wpf
public static TreeViewItem getParentItem(TreeViewItem item)
{
try
{
var parent = VisualTreeHelper.GetParent(item 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;
}
}
private static bool tryFindChild(
int startindex,
TreeViewItem node,
string searchterm,
StringComparison comparison,
out TreeViewItem foundnode
)
{
foundnode = null;
if (!node.IsExpanded) { return false; }
for (int i = startindex; i < node.Items.Count; i++)
{
object item = node.Items[i];
object tviobj = node.ItemContainerGenerator.ContainerFromItem(item);
if (tviobj is null)
{
return false;
}
TreeViewItem tvi = (TreeViewItem)tviobj;
if (checkIfMatchesText(tvi, searchterm, comparison))
{
foundnode = tvi;
return true;
}
//recurse:
if (tryFindChild(tvi, searchterm, comparison, out foundnode))
{
return true;
}
}
return false;
}
private static bool tryFindChild(TreeViewItem node, string searchterm, StringComparison comparison, out TreeViewItem foundnode)
{
return tryFindChild(0, node, searchterm, comparison, out foundnode);
}
public static bool SearchTreeView(TreeViewItem node, string searchterm, StringComparison comparison, out TreeViewItem found)
{
//search children:
if (tryFindChild(node, searchterm, comparison, out found))
{
return true;
}
//search nodes same level as this:
TreeViewItem parent = getParentItem(node);
object boundobj = node.DataContext;
if (!(parent is null || boundobj is null))
{
int startindex = parent.Items.IndexOf(boundobj);
if (tryFindChild(startindex + 1, parent, searchterm, comparison, out found))
{
return true;
}
}
found = null;
return false;
}
}
}
I also save the last selected node, as described in this post:
<TreeView ... TreeViewItem.Selected="TreeViewItemSelected" ... />
private TreeViewItem lastSelectedTreeViewItem;
private void TreeViewItemSelected(object sender, RoutedEventArgs e)
{
TreeViewItem tvi = e.OriginalSource as TreeViewItem;
this.lastSelectedTreeViewItem = tvi;
}
And here's the above TextInput, modified to use this class:
private void treeView_TextInput(object sender, TextCompositionEventArgs e)
{
if ((DateTime.Now - LastSearch).Seconds > 1) { searchterm = ""; }
LastSearch = DateTime.Now;
searchterm += e.Text;
if (lastSelectedTreeViewItem is null)
{
return;
}
TreeViewItem found;
if (TreeViewHelpers.TreeViewItemTextSearcher.SearchTreeView(
node: lastSelectedTreeViewItem,
searchterm: searchterm,
comparison: StringComparison.CurrentCultureIgnoreCase,
out found
))
{
found.IsSelected = true;
found.BringIntoView();
}
}
Note that this solution is a little bit different from the above, in that I only search the children of the selected node, and the nodes at the same level as the selected node.

It is not very straightforward as we expect it to be. But the best solution I have found is here:
http://www.codeproject.com/Articles/26288/Simplifying-the-WPF-TreeView-by-Using-the-ViewMode
Let me know if you need more details.

Related

WPF cannot get ItemContainerGenerator.ContainerFromItem to work

I have looked here and here and many other places, but I just can't seem to get the ItemContainerGenerator.ContainerFromItem method to work on a WPF TreeView! I have tried to pass in the actual item I want to see, but not getting anywhere with that, I just tried to get the first item in my TreeView. Here's my sample code:
private static bool ExpandAndSelectItem(ItemsControl parentContainer, object itemToSelect)
{
// This doesn't work.
parentContainer.BringIntoView();
// May be virtualized, bring into view and try again.
parentContainer.UpdateLayout();
parentContainer.ApplyTemplate();
TreeViewItem topItem = (TreeViewItem)parentContainer.ItemContainerGenerator.ContainerFromItem(parentContainer.Items[0]);
// Can't find child container unless the parent node is Expanded once
if ((topItem != null) && !topItem.IsExpanded)
{
topItem.IsExpanded = true;
parentContainer.UpdateLayout();
}
}
As you can see, I have tried to call many "updating" methods to try to get the TreeView to be "visible" and "accessible". The Catch-22 seems to be that you can't use ContainerFromItem() unless the first TreeViewItem is expanded, but I can't grab the TreeViewItem to Expand it until ContainerFromItem() works!
Another funny thing that is happening is this: When I open this window (it is a UserControl), ContainerFromItem() returns nulls, but if I close the window and open it back up, ContainerFromItem() starts returning non-nulls. Is there any event I should be looking for or forcing to fire?
Turns out the event I was looking for was "Loaded". I just attached an event handler onto my treeview in the XAML, and called my logic in that event handler.
<TreeView x:Name="MyTreeView"
Margin="0,5,0,5"
HorizontalAlignment="Left"
BorderThickness="0"
FontSize="18"
FontFamily="Segoe WP"
MaxWidth="900"
Focusable="True"
Loaded="MyTreeView_Load">
...
</TreeView>
The event handler:
private void MyTreeView_Load(object sender, RoutedEventArgs e)
{
ShowSelectedThing(MyTreeView, ThingToFind);
}
// Gotta call the TreeView an ItemsControl to cast it between TreeView and TreeViewItem
// as you recurse
private static bool ShowSelectedThing(ItemsControl parentContainer, object ThingToFind)
{
// check current level of tree
foreach (object item in parentContainer.Items)
{
TreeViewItem currentContainer = (TreeViewItem)parentContainer.ItemContainerGenerator.ContainerFromItem(item);
if ((currentContainer != null) && (item == ThingToFind)
{
currentContainer.IsSelected = true;
currentContainer.BringIntoView();
return true;
}
}
// item is not found at current level, check the kids
foreach (object item in parentContainer.Items)
{
TreeViewItem currentContainer = (TreeViewItem)parentContainer.ItemContainerGenerator.ContainerFromItem(item);
if ((currentContainer != null) && (currentContainer.Items.Count > 0))
{
// Have to expand the currentContainer or you can't look at the children
currentContainer.IsExpanded = true;
currentContainer.UpdateLayout();
if (!ShowSelectedThing(currentContainer, ThingToFind))
{
// Haven't found the thing, so collapse it back
currentContainer.IsExpanded = false;
}
else
{
// We found the thing
return true;
}
}
}
// default
return false;
}
Hope this helps someone. Sometimes in the real world, with demanding customers, weird requirements and short deadlines, ya gotta hack!
When the container generator's status is 'NotStarted' or 'ContainersGenerating', you can't find the container.
Use this method to find the container of data item.
private static async Task<TreeViewItem> FindItemContainer(ItemsControl itemsControl, object item)
{
var generator = itemsControl.ItemContainerGenerator;
if (generator.Status != GeneratorStatus.ContainersGenerated)
{
var tcs = new TaskCompletionSource<object>();
EventHandler handler = null;
handler = (s, e) =>
{
if (generator.Status == GeneratorStatus.ContainersGenerated)
{
generator.StatusChanged -= handler;
tcs.SetResult(null);
}
else if (generator.Status == GeneratorStatus.Error)
{
generator.StatusChanged -= handler;
tcs.SetException(new InvalidOperationException());
}
};
generator.StatusChanged += handler;
if (itemsControl is TreeViewItem tvi)
tvi.IsExpanded = true;
itemsControl.UpdateLayout();
await tcs.Task;
}
var container = (TreeViewItem)generator.ContainerFromItem(item);
if(container == null)
{
foreach (var parentItem in itemsControl.Items)
{
var parentContainer = (TreeViewItem)generator.ContainerFromItem(parentItem);
container = await FindItemContainer(parentContainer, item);
if (container != null)
return container;
}
}
return container;
}
private void Lv_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
ListView Lv = (ListView)sender;
Lv.UpdateLayout(); // 1.step
DependencyObject Dep = Lv.ItemContainerGenerator
.ContainerFromItem(Lv.SelectedItem);
((ListViewItem)Dep).Focus(); //2.step
}
I had come across this issue time ago and now again I got stuck with it for quite a while. Any MessageBox launch or an expand or dropdown on your particular control type, any of these do the job and start the ItemContainerGenerator. The .UpdateLayout() however is the right thing to do, before the .Focus(). Should be analogous for a Treeview, or one of its Items.

WPF DataGrid MultiColumn sort without shift key

The WPF DataGrid has a default behavior of allowing multicolumn sorts when the user shift-clicks on multiple column headers. Is there a way to change this behavior so that the Shift key is not required? I have already handled the sorting event on the datagrid so that each column will cycle between the three sorting states (Ascending, Descending, and No Sort) and this is all working as expected as long as the shift key is held down, but I would like to make it so that the DataGrid does not reset the sorting on all other columns if the user clicks a column header to add a sort without pressing shift.
I have found a solution that seems to be a bit of a hack, but it works. This article got me pointed in the right direction: http://blogs.msdn.com/b/vinsibal/archive/2008/08/29/wpf-datagrid-tri-state-sorting-sample.aspx?PageIndex=2. That brought me to the understanding the the SortDirection of each column is not really tied to the ItemsSource SortDescriptions in any way. So what I did was subscribe to the Sorting event of the Datagrid and reset the SortDirection of each column that is referenced in the ItemsSource SortDescriptions collection. Apparently, not pressing shift clears the sortdirection of each column, but doesn't reset the SortDescriptions.
I was dealing with the same problem, but no one showed the code that can solve this it. The question is old, but I hope the solution will be useful for seekers.
(DataGrid.Items.SortDescriptions as INotifyCollectionChanged).CollectionChanged += OnGridCollectionChanged;
private void OnGridCollectionChanged(object sender, NotifyCollectionChangedEventArgs notifyCollectionChangedEventArgs)
{
var sortingCollection = (SortDescriptionCollection)sender;
foreach (var sortDesc in sortingCollection)
{
foreach (var column in SignaturesInImagingGrid.Columns)
{
if (column.SortMemberPath.Equals(sortDesc.PropertyName))
{
column.SortDirection = sortDesc.Direction;
}
}
}
}
<DataGrid Sorting="GridMultiColumnSortingEvent">
public static void GridMultiColumnSortingEvent(object sender, DataGridSortingEventArgs e)
{
var dgSender = (DataGrid)sender;
var cView = CollectionViewSource.GetDefaultView(dgSender.ItemsSource);
ListSortDirection direction = ListSortDirection.Ascending;
if (ContainsSortColumn((DataGrid)sender, e.Column.SortMemberPath))
{
if (e.Column.SortDirection == null)
{
direction = ListSortDirection.Ascending;
ChangeSortColumn((DataGrid)sender, e.Column, direction);
}
else if (DirectionForColumn(cView, e.Column) == ListSortDirection.Ascending)
{
direction = ListSortDirection.Descending;
ChangeSortColumn((DataGrid)sender, e.Column, direction);
}
else if (DirectionForColumn(cView, e.Column) == ListSortDirection.Descending)
{
e.Column.SortDirection = null;
cView.SortDescriptions.Remove(cView.SortDescriptions.Where(item => item.PropertyName.Equals(e.Column.SortMemberPath)).FirstOrDefault());
cView.Refresh();
}
}
else
{
AddSortColumn((DataGrid)sender, e.Column.SortMemberPath, direction);
cView.Refresh();
}
e.Handled = true;
}
private static ListSortDirection DirectionForColumn(ICollectionView cView, DataGridColumn column) =>
cView.SortDescriptions.Where(item => item.PropertyName.Equals(column.SortMemberPath))
.FirstOrDefault()
.Direction;
private static void AddSortColumn(DataGrid sender, string sortColumn, ListSortDirection direction)
{
var cView = CollectionViewSource.GetDefaultView(sender.ItemsSource);
cView.SortDescriptions.Add(new SortDescription(sortColumn, direction));
foreach (var col in sender.Columns.Where(x => x.SortMemberPath == sortColumn))
{
col.SortDirection = direction;
}
}
private static void ChangeSortColumn(DataGrid sender, DataGridColumn column, ListSortDirection direction)
{
var cView = CollectionViewSource.GetDefaultView(sender.ItemsSource);
string sortColumn = column.SortMemberPath;
foreach (var sortDesc in cView.SortDescriptions.ToList())
{
if (sortDesc.PropertyName.Equals(sortColumn))
{
cView.SortDescriptions.Remove(sortDesc);
break;
}
}
AddSortColumn(sender, sortColumn, direction);
}
private static bool ContainsSortColumn(DataGrid sender, string sortColumn)
{
var cView = CollectionViewSource.GetDefaultView(sender.ItemsSource);
foreach (var sortDesc in cView.SortDescriptions.ToList())
{
if (sortDesc.PropertyName.Equals(sortColumn))
return true;
}
return false;
}

Wpf Datagrid Max Rows

I am currently working with data grid where I only want to allow the user to enter UP TO 20 rows of data before making CanUserAddRows to false.
I made a dependency property on my own datagrid (that derives from the original one). I tried using the event "ItemContainerGenerator.ItemsChanged" and check for row count in there and if row count = max rows, then make can user add row = false (I get an exception for that saying i'm not allow to change it during "Add Row".
I was wondering if there is a good way on implementing this ....
Thanks and Regards,
Kevin
Here's a subclassed DataGrid with a MaxRows Dependency Property. The things to note about the implementation is if the DataGrid is in edit mode and CanUserAddRows is changed an InvalidOperationException will occur (like you mentioned in the question).
InvalidOperationException
'NewItemPlaceholderPosition' is not
allowed during a transaction begun by
'AddNew'.
To workaround this, a method called IsInEditMode is called in OnItemsChanged and if it returns true we subscribe to the event LayoutUpdated to check IsInEditMode again.
Also, if MaxRows is set to 20, it must allow 20 rows when CanUserAddRows is True, and 19 rows when False (to make place for the NewItemPlaceHolder which isn't present on False).
It can be used like this
<local:MaxRowsDataGrid MaxRows="20"
CanUserAddRows="True"
...>
MaxRowsDataGrid
public class MaxRowsDataGrid : DataGrid
{
public static readonly DependencyProperty MaxRowsProperty =
DependencyProperty.Register("MaxRows",
typeof(int),
typeof(MaxRowsDataGrid),
new UIPropertyMetadata(0, MaxRowsPropertyChanged));
private static void MaxRowsPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
MaxRowsDataGrid maxRowsDataGrid = source as MaxRowsDataGrid;
maxRowsDataGrid.SetCanUserAddRowsState();
}
public int MaxRows
{
get { return (int)GetValue(MaxRowsProperty); }
set { SetValue(MaxRowsProperty, value); }
}
private bool m_changingState;
public MaxRowsDataGrid()
{
m_changingState = false;
}
protected override void OnItemsChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
base.OnItemsChanged(e);
if (IsInEditMode() == true)
{
EventHandler eventHandler = null;
eventHandler += new EventHandler(delegate
{
if (IsInEditMode() == false)
{
SetCanUserAddRowsState();
LayoutUpdated -= eventHandler;
}
});
LayoutUpdated += eventHandler;
}
else
{
SetCanUserAddRowsState();
}
}
private bool IsInEditMode()
{
IEditableCollectionView itemsView = Items;
if (itemsView.IsAddingNew == false && itemsView.IsEditingItem == false)
{
return false;
}
return true;
}
// This method will raise OnItemsChanged again
// because a NewItemPlaceHolder will be added or removed
// so to avoid infinite recursion a bool flag is added
private void SetCanUserAddRowsState()
{
if (m_changingState == false)
{
m_changingState = true;
int maxRows = (CanUserAddRows == true) ? MaxRows : MaxRows-1;
if (Items.Count > maxRows)
{
CanUserAddRows = false;
}
else
{
CanUserAddRows = true;
}
m_changingState = false;
}
}
}
I am an adept of K.I.S.S. To reduce the amount of lines required to do the job and keep it simple, use the following:
private void MyDataGrid_LoadingRow( object sender, DataGridRowEventArgs e )
{
// The user cannot add more rows than allowed
IEditableCollectionView itemsView = this.myDataGrid.Items;
if( this.myDataGrid.Items.Count == max_RowCount + 1 && itemsView.IsAddingNew == true )
{
// Commit the current one added by the user
itemsView.CommitNew();
// Once the adding transaction is commit the user cannot add an other one
this.myDataGrid.CanUserAddRows = false;
}
}
Simple and compact ;0)

Generic method to find all TextBox controls in Silverlight

I have several Silverlight controls on a page and want query all the controls that are of type TextBox and have that working.
Now the Silverlight form I'm working on could have more TextBox controls added. So when I test to see if a TextBox control has a value, I could do:
if (this.TextBox.Control.value.Text() != String.Empty)
{
// do whatever
}
but I'd rather have if flexible that I can use this on ANY Silverlight form regardless of the number of TextBox controls I have.
Any ideas on how I would go about doing that?
I have already faced this issue and notify it here : http://megasnippets.com/en/source-codes/silverlight/Get_all_child_controls_recursively_in_Silverlight
Here you have a generic method to find recursively in the VisualTree all TextBoxes:
IEnumerable<DependencyObject> GetChildrenRecursively(DependencyObject root)
{
List<DependencyObject> children = new List<DependencyObject>();
children.Add(root);
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(root); i++)
children.AddRange(GetChildrenRecursively(VisualTreeHelper.GetChild(root, i)));
return children;
}
Use this method like this to find all TextBoxes:
var textBoxes = GetChildrenRecursively(LayoutRoot).OfType<TextBox>();
It sounds like you need a recursive routine like GetTextBoxes below:
void Page_Loaded(object sender, RoutedEventArgs e)
{
// Instantiate a list of TextBoxes
List<TextBox> textBoxList = new List<TextBox>();
// Call GetTextBoxes function, passing in the root element,
// and the empty list of textboxes (LayoutRoot in this example)
GetTextBoxes(this.LayoutRoot, textBoxList);
// Now textBoxList contains a list of all the text boxes on your page.
// Find all the non empty textboxes, and put them into a list.
var nonEmptyTextBoxList = textBoxList.Where(txt => txt.Text != string.Empty).ToList();
// Do something with each non empty textbox.
nonEmptyTextBoxList.ForEach(txt => Debug.WriteLine(txt.Text));
}
private void GetTextBoxes(UIElement uiElement, List<TextBox> textBoxList)
{
TextBox textBox = uiElement as TextBox;
if (textBox != null)
{
// If the UIElement is a Textbox, add it to the list.
textBoxList.Add(textBox);
}
else
{
Panel panel = uiElement as Panel;
if (panel != null)
{
// If the UIElement is a panel, then loop through it's children
foreach (UIElement child in panel.Children)
{
GetTextBoxes(child, textBoxList);
}
}
}
}
Instantiate an empty list of TextBoxes. Call GetTextBoxes, passing in the root control on your page (in my case, that's this.LayoutRoot), and GetTextBoxes should recursively loop through every UI element that is a descendant of that control, testing to see if it's either a TextBox (add it to the list), or a panel, that might have descendants of it's own to recurse through.
Hope that helps. :)
From your top most panel you can do this (my grid is called ContentGrid)
var textBoxes = this.ContentGrid.Children.OfType<TextBox>();
var nonEmptyTextboxes = textBoxes.Where(t => !String.IsNullOrEmpty(t.Text));
foreach (var textBox in nonEmptyTextboxes)
{
//Do Something
}
However this will only find the textboxes that are immediate children. Some sort of recursion like below would help, but I'm thinking there must be a better way.
private List<TextBox> SearchForTextBoxes(Panel panel)
{
List<TextBox> list = new List<TextBox>();
list.AddRange(panel.Children.OfType<TextBox>()
.Where(t => !String.IsNullOrEmpty(t.Text)));
var panels = panel.Children.OfType<Panel>();
foreach (var childPanel in panels)
{
list.AddRange(SearchForTextBoxes(childPanel));
}
return list;
}
Took Scott's initial idea and expanded it so that it
Uses generics, so it easily copes with multiple control types.
Supports more container types. In my WP7 I needed to support panaorama's, scroll viewers etc... which aren't Panels. So this allows support for them.
Biggest issue is that string comparing, especially on the Panel and derrived items.
Code:
private static void GetControls<T>(UIElement uiElement, List<T> controlList) where T : UIElement
{
var frameworkFullName = uiElement.GetType().FullName;
if (frameworkFullName == typeof(T).FullName)
{
controlList.Add(uiElement as T);
return;
}
if (frameworkFullName == typeof(Panel).FullName ||
frameworkFullName == typeof(Grid).FullName ||
frameworkFullName == typeof(StackPanel).FullName)
{
foreach (var child in (uiElement as Panel).Children)
{
GetControls(child, controlList);
}
return;
}
if (frameworkFullName == typeof(Panorama).FullName)
{
foreach (PanoramaItem child in (uiElement as Panorama).Items)
{
var contentElement = child.Content as FrameworkElement;
if (contentElement != null)
{
GetControls(contentElement, controlList);
}
}
return;
}
if (frameworkFullName == typeof(ScrollViewer).FullName)
{
var contentElement = (uiElement as ScrollViewer).Content as FrameworkElement;
if (contentElement != null)
{
GetControls(contentElement, controlList);
}
return;
}
}
Similar logic to ideas above to also handle controls with a "Content" attribute like TabItems and Scrollviewers where children might be embedded at a lower level. Finds all children:
IEnumerable<DependencyObject> GetControlsRecursive(DependencyObject root)
{
List<DependencyObject> elts = new List<DependencyObject>();
elts.Add(root);
string type = root.GetType().ToString().Replace("System.Windows.Controls.", "");
switch (root.GetType().ToString().Replace("System.Windows.Controls.", ""))
{
case "TabItem":
var TabItem = (TabItem)root;
elts.AddRange(GetControlsRecursive((DependencyObject)TabItem.Content));
break;
case "ScrollViewer":
var Scroll = (ScrollViewer)root;
elts.AddRange(GetControlsRecursive((DependencyObject) Scroll.Content));
break;
default: //controls that have visual children go here
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(root); i++) elts.AddRange(GetControlsRecursive(VisualTreeHelper.GetChild(root, i)));
break;
}
return elts;
}

Detecting WPF Validation Errors

In WPF you can setup validation based on errors thrown in your Data Layer during Data Binding using the ExceptionValidationRule or DataErrorValidationRule.
Suppose you had a bunch of controls set up this way and you had a Save button. When the user clicks the Save button, you need to make sure there are no validation errors before proceeding with the save. If there are validation errors, you want to holler at them.
In WPF, how do you find out if any of your Data Bound controls have validation errors set?
This post was extremely helpful. Thanks to all who contributed. Here is a LINQ version that you will either love or hate.
private void CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = IsValid(sender as DependencyObject);
}
private bool IsValid(DependencyObject obj)
{
// The dependency object is valid if it has no errors and all
// of its children (that are dependency objects) are error-free.
return !Validation.GetHasError(obj) &&
LogicalTreeHelper.GetChildren(obj)
.OfType<DependencyObject>()
.All(IsValid);
}
The following code (from Programming WPF book by Chris Sell & Ian Griffiths) validates all binding rules on a dependency object and its children:
public static class Validator
{
public static bool IsValid(DependencyObject parent)
{
// Validate all the bindings on the parent
bool valid = true;
LocalValueEnumerator localValues = parent.GetLocalValueEnumerator();
while (localValues.MoveNext())
{
LocalValueEntry entry = localValues.Current;
if (BindingOperations.IsDataBound(parent, entry.Property))
{
Binding binding = BindingOperations.GetBinding(parent, entry.Property);
foreach (ValidationRule rule in binding.ValidationRules)
{
ValidationResult result = rule.Validate(parent.GetValue(entry.Property), null);
if (!result.IsValid)
{
BindingExpression expression = BindingOperations.GetBindingExpression(parent, entry.Property);
System.Windows.Controls.Validation.MarkInvalid(expression, new ValidationError(rule, expression, result.ErrorContent, null));
valid = false;
}
}
}
}
// Validate all the bindings on the children
for (int i = 0; i != VisualTreeHelper.GetChildrenCount(parent); ++i)
{
DependencyObject child = VisualTreeHelper.GetChild(parent, i);
if (!IsValid(child)) { valid = false; }
}
return valid;
}
}
You can call this in your save button click event handler like this in your page/window
private void saveButton_Click(object sender, RoutedEventArgs e)
{
if (Validator.IsValid(this)) // is valid
{
....
}
}
The posted code did not work for me when using a ListBox. I rewrote it and now it works:
public static bool IsValid(DependencyObject parent)
{
if (Validation.GetHasError(parent))
return false;
// Validate all the bindings on the children
for (int i = 0; i != VisualTreeHelper.GetChildrenCount(parent); ++i)
{
DependencyObject child = VisualTreeHelper.GetChild(parent, i);
if (!IsValid(child)) { return false; }
}
return true;
}
Had the same problem and tried the provided solutions. A combination of H-Man2's and skiba_k's solutions worked almost fine for me, for one exception: My Window has a TabControl. And the validation rules only get evaluated for the TabItem that is currently visible. So I replaced VisualTreeHelper by LogicalTreeHelper. Now it works.
public static bool IsValid(DependencyObject parent)
{
// Validate all the bindings on the parent
bool valid = true;
LocalValueEnumerator localValues = parent.GetLocalValueEnumerator();
while (localValues.MoveNext())
{
LocalValueEntry entry = localValues.Current;
if (BindingOperations.IsDataBound(parent, entry.Property))
{
Binding binding = BindingOperations.GetBinding(parent, entry.Property);
if (binding.ValidationRules.Count > 0)
{
BindingExpression expression = BindingOperations.GetBindingExpression(parent, entry.Property);
expression.UpdateSource();
if (expression.HasError)
{
valid = false;
}
}
}
}
// Validate all the bindings on the children
System.Collections.IEnumerable children = LogicalTreeHelper.GetChildren(parent);
foreach (object obj in children)
{
if (obj is DependencyObject)
{
DependencyObject child = (DependencyObject)obj;
if (!IsValid(child)) { valid = false; }
}
}
return valid;
}
In addition to the great LINQ-implementation of Dean, I had fun wrapping the code into an extension for DependencyObjects:
public static bool IsValid(this DependencyObject instance)
{
// Validate recursivly
return !Validation.GetHasError(instance) && LogicalTreeHelper.GetChildren(instance).OfType<DependencyObject>().All(child => child.IsValid());
}
This makes it extremely nice considering reuseablity.
I would offer a small optimization.
If you do this many times over the same controls, you can add the above code to keep a list of controls that actually have validation rules. Then whenever you need to check for validity, only go over those controls, instead of the whole visual tree.
This would prove to be much better if you have many such controls.
Here is a library for form validation in WPF. Nuget package here.
Sample:
<Border BorderBrush="{Binding Path=(validationScope:Scope.HasErrors),
Converter={local:BoolToBrushConverter},
ElementName=Form}"
BorderThickness="1">
<StackPanel x:Name="Form" validationScope:Scope.ForInputTypes="{x:Static validationScope:InputTypeCollection.Default}">
<TextBox Text="{Binding SomeProperty}" />
<TextBox Text="{Binding SomeOtherProperty}" />
</StackPanel>
</Border>
The idea is that we define a validation scope via the attached property telling it what input controls to track.
Then we can do:
<ItemsControl ItemsSource="{Binding Path=(validationScope:Scope.Errors),
ElementName=Form}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type ValidationError}">
<TextBlock Foreground="Red"
Text="{Binding ErrorContent}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
You can iterate over all your controls tree recursively and check the attached property Validation.HasErrorProperty, then focus on the first one you find in it.
you can also use many already-written solutions
you can check this thread for an example and more information
You might be interested in the BookLibrary sample application of the WPF Application Framework (WAF). It shows how to use validation in WPF and how to control the Save button when validation errors exists.
In answer form aogan, instead of explicitly iterate through validation rules, better just invoke expression.UpdateSource():
if (BindingOperations.IsDataBound(parent, entry.Property))
{
Binding binding = BindingOperations.GetBinding(parent, entry.Property);
if (binding.ValidationRules.Count > 0)
{
BindingExpression expression
= BindingOperations.GetBindingExpression(parent, entry.Property);
expression.UpdateSource();
if (expression.HasError) valid = false;
}
}
I am using a DataGrid, and the normal code above did not find errors until the DataGrid itself lost focus. Even with the code below, it still doesn't "see" an error until the row loses focus, but that's at least better than waiting until the grid loses focus.
This version also tracks all errors in a string list. Most of the other version in this post do not do that, so they can stop on the first error.
public static List<string> Errors { get; set; } = new();
public static bool IsValid(this DependencyObject parent)
{
Errors.Clear();
return IsValidInternal(parent);
}
private static bool IsValidInternal(DependencyObject parent)
{
// Validate all the bindings on this instance
bool valid = true;
if (Validation.GetHasError(parent) ||
GetRowsHasError(parent))
{
valid = false;
/*
* Find the error message and log it in the Errors list.
*/
foreach (var error in Validation.GetErrors(parent))
{
if (error.ErrorContent is string errorMessage)
{
Errors.Add(errorMessage);
}
else
{
if (parent is Control control)
{
Errors.Add($"<unknow error> on field `{control.Name}`");
}
else
{
Errors.Add("<unknow error>");
}
}
}
}
// Validate all the bindings on the children
for (int i = 0; i != VisualTreeHelper.GetChildrenCount(parent); i++)
{
var child = VisualTreeHelper.GetChild(parent, i);
if (IsValidInternal(child) == false)
{
valid = false;
}
}
return valid;
}
private static bool GetRowsHasError(DependencyObject parent)
{
DataGridRow dataGridRow;
if (parent is not DataGrid dataGrid)
{
/*
* This is not a DataGrid, so return and say we do not have an error.
* Errors for this object will be checked by the normal check instead.
*/
return false;
}
foreach (var item in dataGrid.Items)
{
/*
* Not sure why, but under some conditions I was returned a null dataGridRow
* so I had to test for it.
*/
dataGridRow = (DataGridRow)dataGrid.ItemContainerGenerator.ContainerFromItem(item);
if (dataGridRow != null &&
Validation.GetHasError(dataGridRow))
{
return true;
}
}
return false;
}

Resources