Controlling a TextBox's scroll position from the ViewModel? - c#

Is there a solution that involves a WPF TextBox/Block that auto scrolls to the end via binding? This can obviously be done in the code behind by calling the control directly, but how would one do this with Binding and MVVM?
in the code behind that works (but I'd like to avoid that and use the VM to do everything)
public void _readerService_BytesArrived(string s)
{
Action dispatcherAction = () =>
{
txtBoxOutPut.AppendText(s);
txtBoxOutPut.ScrollToEnd();
};
Dispatcher.Invoke(dispatcherAction);
}

I'm thinking that you're attempting to scroll to the end when the value of the Text changes inside of a TextBox/Block. Since this is a view-related operation, it should stay that way. Simply place a TextChanged event on the control and scroll to the end when the Text property changes.
Note that this basically means you need to split your operation... keep the binding on the view-model end, and place the ScrollToEnd in code-behind in your view... the view-model shouldn't care who is consuming the text string and how they behave.

An System.Windows.Interactivity Behavior may do just the trick for you. I use them for scrolling various controls and it's not in the VM but it's also not in the View and follows MVVM.
Below is an example for a Scrollviewer that may be helpful
public class FrameworkElementScrollviewerScrollingBehavior : Behavior<FrameworkElement>
{
private FrameworkElement _AssociatedElement;
private ScrollViewer _listboxScrollViewer = null;
#region OnAttached
protected override void OnAttached()
{
base.OnAttached();
_AssociatedElement = AssociatedObject;
_AssociatedElement.Loaded += OnControlLoaded;
_AssociatedElement.Unloaded += new RoutedEventHandler(_AssociatedElement_Unloaded);
//TODO: register/subscrive for event/message from the VM that tells you the scrollviewer to do something
}
//TODO: handle the event using the _AssociatedElement as the control you are acting on
void _AssociatedElement_Unloaded(object sender, RoutedEventArgs e)
{
Cleanup();
}
#endregion
#region OnDetaching
protected override void OnDetaching()
{
Cleanup();
base.OnDetaching();
}
#endregion
private bool _isCleanedUp;
private void Cleanup()
{
if (!_isCleanedUp)
{
_AssociatedElement.Loaded -= OnControlLoaded;
_AssociatedElement.Unloaded -= _AssociatedElement_Unloaded;
}
}
#region OnControlLoaded
private void OnControlLoaded(object sender, RoutedEventArgs args)
{
FrameworkElement element = sender as FrameworkElement;
if (element != null)
{
_listboxScrollViewer = GetDescendantByType(sender as Visual, typeof(ScrollViewer)) as ScrollViewer;
if (_listboxScrollViewer.ComputedVerticalScrollBarVisibility == Visibility.Visible)
//do something when content is scrollable
}
}
#endregion
#region GetDescendantByType
/// <summary>
/// Gets the descendent of type
/// </summary>
/// <param name="element">The element.</param>
/// <param name="type">The type.</param>
/// <returns></returns>
public static Visual GetDescendantByType(Visual element, Type type)
{
if (element == null) return null;
if (element.GetType() == type) return element;
Visual foundElement = null;
if (element is FrameworkElement)
(element as FrameworkElement).ApplyTemplate();
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(element); i++)
{
Visual visual = VisualTreeHelper.GetChild(element, i) as Visual;
foundElement = GetDescendantByType(visual, type);
if (foundElement != null)
break;
}
return foundElement;
}
#endregion
}

Related

ComboBox with both filter and auto-complete

Has anyone succeeded with WPF's ComboBox auto-complete and filter functionalities? I have spent several hours now and haven't been able to nailed it. This is WPF + MVVM Light. Here is my setup.
VM layer
A ViewModel that provides the following properties:
FilterText(string): Text that user has typed in TextBox area for filtering. Fires change notification on FilteredItems.
Items(List<string>): This is the main data source containing all options.
FilteredItems: Filtered list of Items using FilterText.
SelectedOption (string): Currently selected option.
View layer
A ComboBox where user can choose from the drop-down options only. However, user should be allowed to type text in the textbox area and the dropdown should filter out items that do not begin with typed text. First matching item should automatically be appended to the textbox (auto-complete that is). Here are my bindings:
ItemsSource: binds to FilteredItems, One-way
Text binds to FilterText, Two-way
SelectedItem binds to SelectedOption, Two-way
IsTextSearchEnabled is set to true for enabling auto-complete.
Problem with this setup is that as soon as user types the first letter, auto-complete is triggered and tries to locate the first matching entry and if found, sets SelectedItem to that entry, which in set the Text property of the ComboBox to that item, which in turn triggers filter operation and the dropdown is left with only one entry that fully matches Text, which is not what it should be like.
For example, if user types "C", auto-complete will try to locate first entry starting with "C". Let's say that the first matching entry is "Customer". Auto-complete will select that entry, which will set SelectedItem to "Customer" and therefore Text will also become "Customer. This will invoke FilterText because of binding, which will update FilteredItems, which will now return only one entry, instead of returning all entries starting with "C".
What am I missing here?
I think your approach is too complicated.
You can implement a simple Attached Behavior to achieve a filtered suggestion list while autocomplete is enabled.
This example doesn't require any additional properties except the common source collection for the ComboBox.ItemsSource. The filtering is done by using the ICollectionView.Filter property. This will modify only the view of the internal source collection of the ItemsControl, but not the underlying binding source collection itself. Setting IsTextSearchEnabled to True is not required to enable autocomplete.
The basic idea is to trigger the filtering rather on TextBox.TextChanged than on ComboBox.SelectedItemChanged (or ComboBox.SelectedItem in general).
ComboBox.cs
class ComboBox : DependencyObject
{
#region IsFilterOnAutoCompleteEnabled attached property
public static readonly DependencyProperty IsFilterOnAutocompleteEnabledProperty =
DependencyProperty.RegisterAttached(
"IsFilterOnAutocompleteEnabled",
typeof(bool),
typeof(ComboBox),
new PropertyMetadata(default(bool), ComboBox.OnIsFilterOnAutocompleteEnabledChanged));
public static void SetIsFilterOnAutocompleteEnabled(DependencyObject attachingElement, bool value) =>
attachingElement.SetValue(ComboBox.IsFilterOnAutocompleteEnabledProperty, value);
public static bool GetIsFilterOnAutocompleteEnabled(DependencyObject attachingElement) =>
(bool)attachingElement.GetValue(ComboBox.IsFilterOnAutocompleteEnabledProperty);
#endregion
// Use hash tables for faster lookup
private static Dictionary<TextBox, System.Windows.Controls.ComboBox> TextBoxComboBoxMap { get; }
private static Dictionary<TextBox, int> TextBoxSelectionStartMap { get; }
private static Dictionary<System.Windows.Controls.ComboBox, TextBox> ComboBoxTextBoxMap { get; }
private static bool IsNavigationKeyPressed { get; set; }
static ComboBox()
{
ComboBox.TextBoxComboBoxMap = new Dictionary<TextBox, System.Windows.Controls.ComboBox>();
ComboBox.TextBoxSelectionStartMap = new Dictionary<TextBox, int>();
ComboBox.ComboBoxTextBoxMap = new Dictionary<System.Windows.Controls.ComboBox, TextBox>();
}
private static void OnIsFilterOnAutocompleteEnabledChanged(
DependencyObject attachingElement,
DependencyPropertyChangedEventArgs e)
{
if (!(attachingElement is System.Windows.Controls.ComboBox comboBox
&& comboBox.IsEditable))
{
return;
}
if (!(bool)e.NewValue)
{
ComboBox.DisableAutocompleteFilter(comboBox);
return;
}
if (!comboBox.IsLoaded)
{
comboBox.Loaded += ComboBox.EnableAutocompleteFilterOnComboBoxLoaded;
return;
}
ComboBox.EnableAutocompleteFilter(comboBox);
}
private static async void FilterOnTextInput(object sender, TextChangedEventArgs e)
{
await Application.Current.Dispatcher.InvokeAsync(
() =>
{
if (ComboBox.IsNavigationKeyPressed)
{
return;
}
var textBox = sender as TextBox;
int textBoxSelectionStart = textBox.SelectionStart;
ComboBox.TextBoxSelectionStartMap[textBox] = textBoxSelectionStart;
string changedTextOnAutocomplete = textBox.Text.Substring(0, textBoxSelectionStart);
if (ComboBox.TextBoxComboBoxMap.TryGetValue(
textBox,
out System.Windows.Controls.ComboBox comboBox))
{
comboBox.Items.Filter = item => item.ToString().StartsWith(
changedTextOnAutocomplete,
StringComparison.OrdinalIgnoreCase);
}
},
DispatcherPriority.Background);
}
private static async void HandleKeyDownWhileFiltering(object sender, KeyEventArgs e)
{
var comboBox = sender as System.Windows.Controls.ComboBox;
if (!ComboBox.ComboBoxTextBoxMap.TryGetValue(comboBox, out TextBox textBox))
{
return;
}
switch (e.Key)
{
case Key.Down
when comboBox.Items.CurrentPosition < comboBox.Items.Count - 1
&& comboBox.Items.MoveCurrentToNext():
case Key.Up
when comboBox.Items.CurrentPosition > 0
&& comboBox.Items.MoveCurrentToPrevious():
{
// Prevent the filter from re-apply as this would override the
// current selection start index
ComboBox.IsNavigationKeyPressed = true;
// Ensure the Dispatcher en-queued delegate
// (and the invocation of the SelectCurrentItem() method)
// executes AFTER the FilterOnTextInput() event handler.
// This is because key input events have a higher priority
// than text change events by default. The goal is to make the filtering
// triggered by the TextBox.TextChanged event ignore the changes
// introduced by this KeyDown event.
// DispatcherPriority.ContextIdle will force to "override" this behavior.
await Application.Current.Dispatcher.InvokeAsync(
() =>
{
ComboBox.SelectCurrentItem(textBox, comboBox);
ComboBox.IsNavigationKeyPressed = false;
},
DispatcherPriority.ContextIdle);
break;
}
}
}
private static void SelectCurrentItem(TextBox textBox, System.Windows.Controls.ComboBox comboBox)
{
comboBox.SelectedItem = comboBox.Items.CurrentItem;
if (ComboBox.TextBoxSelectionStartMap.TryGetValue(textBox, out int selectionStart))
{
textBox.SelectionStart = selectionStart;
}
}
private static void EnableAutocompleteFilterOnComboBoxLoaded(object sender, RoutedEventArgs e)
{
var comboBox = sender as System.Windows.Controls.ComboBox;
ComboBox.EnableAutocompleteFilter(comboBox);
}
private static void EnableAutocompleteFilter(System.Windows.Controls.ComboBox comboBox)
{
if (comboBox.TryFindVisualChildElement(out TextBox editTextBox))
{
ComboBox.TextBoxComboBoxMap.Add(editTextBox, comboBox);
ComboBox.ComboBoxTextBoxMap.Add(comboBox, editTextBox);
editTextBox.TextChanged += ComboBox.FilterOnTextInput;
// Need to receive handled KeyDown event
comboBox.AddHandler(UIElement.PreviewKeyDownEvent, new KeyEventHandler(HandleKeyDownWhileFiltering), true);
}
}
private static void DisableAutocompleteFilter(System.Windows.Controls.ComboBox comboBox)
{
if (comboBox.TryFindVisualChildElement(out TextBox editTextBox))
{
ComboBox.TextBoxComboBoxMap.Remove(editTextBox);
editTextBox.TextChanged -= ComboBox.FilterOnTextInput;
}
}
}
Extensions.cs
public static class Extensions
{
/// <summary>
/// Traverses the visual tree towards the leafs until an element with a matching element type is found.
/// </summary>
/// <typeparam name="TChild">The type the visual child must match.</typeparam>
/// <param name="parent"></param>
/// <param name="resultElement"></param>
/// <returns></returns>
public static bool TryFindVisualChildElement<TChild>(this DependencyObject parent, out TChild resultElement)
where TChild : DependencyObject
{
resultElement = null;
if (parent is Popup popup)
{
parent = popup.Child;
if (parent == null)
{
return false;
}
}
for (var childIndex = 0; childIndex < VisualTreeHelper.GetChildrenCount(parent); childIndex++)
{
DependencyObject childElement = VisualTreeHelper.GetChild(parent, childIndex);
if (childElement is TChild child)
{
resultElement = child;
return true;
}
if (childElement.TryFindVisualChildElement(out resultElement))
{
return true;
}
}
return false;
}
}
Usage Example
<ComboBox ItemsSource="{Binding Items}"
IsEditable="True"
ComboBox.IsFilterOnAutocompleteEnabled="True" />

How to create a Editable Spinner (Picker)?

I want to create a editable Spinner (or Picker in Xamarin.Forms).
I have a custom renderer for my element (derived from Picker) that render the Picker as AutoCompleteTextView. Inside the renderer i have created AutoCompleteTextView that shows the dropdown menue if it on focus or is been clicked. Its worked fine.
My problem is that it shows like a EditText (or Entry in Xamarin.Forms) control on device, but i want to display it like a Spinner (or Picker on Xamarin.Forms).
Any idea how i have to make this?
EDIT:
Here what i do in UWP:
Custom Renderer for UWP control:
CustomEditablePicker customControl; // Derived from Xamarin.Forms.Picker
ComboBox nativeControl; // Windows.UI.Xaml.Controls.ComboBox
TextBox editControl;
protected override void OnElementChanged(ElementChangedEventArgs<CustomEditablePicker> e)
{
base.OnElementChanged(e);
customControl = e.NewElement;
nativeControl = new ComboBox();
editControl = new TextBox(); // First element of CheckBox would be a TextBox for edit some Text
// Set the style (declarated in App.xaml)
Style editableStyle = App.Current.Resources["ComboBoxItemTextBox"] as Style;
if (editableStyle != null)
{
editControl.Style = editableStyle;
ComboBoxItem item = new ComboBoxItem();
item.IsSelected = true; // Select First element
item.Content = editControl; // TextBox as content for first element
nativeControl.Items.Add(item);
nativeControl.SelectionChanged += NativeControl_SelectionChanged; // Do something if selection is changed
}
// Add items from custom element to native element
foreach (var item in customControl.Items)
{
nativeControl.Items.Add(item);
}
editControl.KeyDown += EditControl_KeyDown; // Handle the space key
editControl.TextChanged += EditControl_TextChanged; // Handle something if text inside TextBox is changed
base.SetNativeControl(nativeControl); // Set native control to be displayed
}
/// <summary>
/// Set text for Picker if value is changed
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void EditControl_TextChanged(object sender, TextChangedEventArgs e)
{
TextBox edit = (sender as TextBox);
customControl.Text = edit.Text;
}
/// <summary>
/// Handle Space-Key, without handle this key the ComboBox would be lost focus
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void EditControl_KeyDown(object sender, KeyRoutedEventArgs e)
{
if (e.Key == Windows.System.VirtualKey.Space)
{
if (editControl.SelectionLength > 0)
{
editControl.Text = editControl.Text.Remove(editControl.SelectionStart, editControl.SelectionLength);
editControl.SelectionLength = 0;
}
int pos = editControl.SelectionStart;
editControl.Text = editControl.Text.Insert(pos, " ");
editControl.SelectionStart = pos + 1;
e.Handled = true;
}
}
/// <summary>
/// Occurs when selection of the box is changed
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void NativeControl_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.AddedItems.Count == 1 && e.AddedItems[0] != (sender as ComboBox).Items[0])
{
(sender as ComboBox).SelectedIndex = 0;
editControl.Text = e.AddedItems[0] as String;
}
}
And The control in the PCL (Xamarin.Forms):
public class CustomEditablePicker : Picker
{
public static readonly BindableProperty EditTextProperty = BindableProperty.Create<CustomEditablePicker, string>(c => c.Text, String.Empty, BindingMode.TwoWay, propertyChanged: OnTextChanged);
public event EventHandler<CustomUIEventArgs<string>> TextChanged;
public static readonly BindableProperty Source = BindableProperty.Create<CustomEditablePicker, IEnumerable<string>>(l => l.ItemsSource, new List<string>(), BindingMode.TwoWay, propertyChanged: OnSourceChanged);
private static void OnSourceChanged(BindableObject bindable, IEnumerable<string> oldValue, IEnumerable<string> newValue)
{
CustomEditablePicker customEditablePicker = (CustomEditablePicker)bindable;
customEditablePicker.ItemsSource = newValue;
}
public event EventHandler<CustomUIEnumerableArgs<IEnumerable<string>>> SourceChanged;
public IEnumerable<string> ItemsSource
{
get { return (List<string>)this.GetValue(Source); }
set
{
if (this.ItemsSource != value)
{
this.SetValue(Source, value);
if (SourceChanged != null)
{
this.SourceChanged.Invoke(this, new CustomUIEnumerableArgs<IEnumerable<string>>(value));
}
}
}
}
public string Text
{
get { return (string)this.GetValue(EditTextProperty); }
set
{
if (this.Text != value)
{
this.SetValue(EditTextProperty, value);
if (TextChanged != null)
{
// Raise a event, with changed text
this.TextChanged.Invoke(this, new CustomUIEventArgs<string>(value));
}
}
}
}
private static void OnTextChanged(BindableObject bindable, string oldValue, string newValue)
{
CustomEditablePicker customEditablePicker = (CustomEditablePicker)bindable;
customEditablePicker.Text = newValue;
}
}
To show image inside EditText, use SetCompoundDrawablesWithIntrinsicBounds:
protected override void OnElementChanged(ElementChangedEventArgs<SoundsPicker> e)
{
if (e.NewElement != null)
{
if (base.Control == null)
{
EditText editText = new EditText(Context)
{
Focusable = false,
Clickable = true,
Tag = this
};
var padding = (int)Context.ToPixels(10);
// that show image on right side
editText.SetCompoundDrawablesWithIntrinsicBounds(0, 0, Resource.Drawable.arrow_down, 0);
editText.CompoundDrawablePadding = padding;
editText.SetOnClickListener(MyPickerPickerListener.Instance);
editText.SetBackgroundDrawable(null);
SetNativeControl(editText);
}
}
base.OnElementChanged(e);
}
Where is Resource.Drawable.arrow_down is your arrow image.
You can use tools like ILSpy or dotPeek to look at code inside Xamarin assembly.

Inject / Navigate RibbonTabItems depending on selected / loaded module

I have got following problem:
There is a Ribbon-region in the Shell, let´s call it the "ShellRibbonRegion". There is also a task button region "ShellTaskButtonRegion" (similar to Creating View-Switching Applications with Prism 4).
There are e.g. three modules. Each module has a different number of RibbonTabItems. Module 1 has one RibbonTabItem, module 2 four and module 3 one.
The goal is now to add the RibbonTabItems to the "ShellRibbonRegion" after the "TaskButton" of a module is clicked. I already have written a custom RegionAdapter, but the problem is either only one RibbonTabItem (SingleActiveRegion) is shown or all (AllActiveRegion) RibbonTabItems from all modules.
public class RibbonRegionAdapter : RegionAdapterBase<Ribbon>
{
/// <summary>
/// Default constructor.
/// </summary>
/// <param name="behaviorFactory">Allows the registration of the default set of RegionBehaviors.</param>
public RibbonRegionAdapter(IRegionBehaviorFactory behaviorFactory)
: base(behaviorFactory)
{
}
/// <summary>
/// Adapts a WPF control to serve as a Prism IRegion.
/// </summary>
/// <param name="region">The new region being used.</param>
/// <param name="regionTarget">The WPF control to adapt.</param>
protected override void Adapt(IRegion region, Ribbon regionTarget)
{
regionTarget.SelectedTabChanged += (sender, args) =>
{
if (regionTarget.SelectedTabItem == null)
return;
//region.Activate(regionTarget.SelectedTabItem);
};
region.Views.CollectionChanged += (sender, e) =>
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
foreach (UIElement element in e.NewItems)
{
if(element is Ribbon)
this.AddRibbon(element as Ribbon, regionTarget, region);
else if(element is RibbonTabItem)
this.AddRibbonTabItem(element as RibbonTabItem, regionTarget, region);
else if(element is Backstage)
this.AddBackstage(element as Backstage, regionTarget);
}
break;
case NotifyCollectionChangedAction.Remove:
foreach (UIElement elementLoopVariable in e.OldItems)
{
var element = elementLoopVariable;
if (element is Ribbon)
this.RemoveRibbon(element as Ribbon, regionTarget);
else if (element is RibbonTabItem)
this.RemoveRibbonTabItem(element as RibbonTabItem, regionTarget);
else if (element is Backstage)
this.RemoveBackstage(element as Backstage, regionTarget);
}
break;
}
};
}
#region Add
private void AddRibbon(Ribbon ribbon, Ribbon targetRibbon, IRegion region)
{
//Add tabs
foreach (var ribbonTabItem in ribbon.Tabs)
{
this.AddRibbonTabItem(ribbonTabItem, targetRibbon, region);
}
}
private void AddRibbonTabItem(RibbonTabItem ribbonTabItem, Ribbon targetRibbon, IRegion region)
{
if (!targetRibbon.Tabs.Contains(ribbonTabItem))
targetRibbon.Tabs.Add(ribbonTabItem);
}
private void AddBackstage(Backstage backstage, Ribbon targetRibbon)
{
}
#endregion
#region Remove
private void RemoveRibbon(Ribbon ribbon, Ribbon targetRibbon)
{
var tmp = new List<RibbonTabItem>(ribbon.Tabs);
//Add tabs
foreach (var ribbonTabItem in tmp)
{
if (targetRibbon.Tabs.Contains(ribbonTabItem)) this.RemoveRibbonTabItem(ribbonTabItem, targetRibbon);
}
}
private void RemoveRibbonTabItem(RibbonTabItem ribbonTabItem, Ribbon targetRibbon)
{
if (ribbonTabItem is IRegionMemberLifetime)
{
var rml = (IRegionMemberLifetime)ribbonTabItem;
if (!rml.KeepAlive) targetRibbon.Tabs.Remove(ribbonTabItem);
return;
}
targetRibbon.Tabs.Remove(ribbonTabItem);
}
private void RemoveBackstage(Backstage backstage, Ribbon targetRibbon)
{
}
#endregion
protected override IRegion CreateRegion()
{
return new AllActiveRegion();
}
}
The desired behaviour is following:
The "TaskButton" of a module is clicked: All RibbonTabItems which don´t belong to this module are removed from the region and the tab items from "clicked" module are added.
How can I achieve this behaviour?
I had a similar problem and ended up keeping track of tabs added in a hashtable keyed by the module type name. The module tabs were developed in module views containing ribbon controls so the datacontext also needed to be transferred. Here is what I ended up with as my RibbonRegionAdapter (add your own namespace!):
// Based on
// http://www.codeproject.com/Articles/165370/Creating-View-Switching-Applications-with-Prism-4#AppendixA
// with my modifications.
/// <summary>
/// Enables use of a Ribbon control as a Prism region.
/// </summary>
/// <remarks> See Developer's Guide to Microsoft Prism (Ver. 4), p. 189.</remarks>
[Export]
public class RibbonRegionAdapter : RegionAdapterBase<Ribbon> {
private static readonly Hashtable RibbonTabs = new Hashtable();
/// <summary>
/// Default constructor.
/// </summary>
/// <param name="behaviorFactory">Allows the registration
/// of the default set of RegionBehaviors.</param>
[ImportingConstructor]
public RibbonRegionAdapter(IRegionBehaviorFactory behaviorFactory)
: base(behaviorFactory) {}
/// <summary>
/// Adapts a WPF control to serve as a Prism IRegion.
/// </summary>
/// <param name="region">The new region being used.</param>
/// <param name="regionTarget">The WPF control to adapt.</param>
protected override void Adapt(IRegion region, Ribbon regionTarget) {
region.Views.CollectionChanged += (sender, e) => {
switch (e.Action) {
case NotifyCollectionChangedAction.Add:
foreach (FrameworkElement element in e.NewItems) {
if (element is Ribbon) {
Ribbon rb = element as Ribbon;
var tabList = new List<RibbonTab>();
var items = rb.Items;
for (int i = rb.Items.Count - 1; i >= 0; i--) {
if (!(rb.Items[i] is RibbonTab)) continue;
RibbonTab rt = (rb.Items[i] as RibbonTab);
rb.Items.Remove(rt); // remove from existing view ribbon
regionTarget.Items.Add(rt); // add to target region ribbon
tabList.Add(rt); // add to tracking list
// Without these next 3 lines the tabs datacontext would end up being inherited from the Ribbon to which
// it has been transferred.
// Not sure if this is the best place to do this but it works for my purposes at the moment
if (rt.DataContext.Equals(regionTarget.DataContext)) { // then it is inherited
rt.DataContext = rb.DataContext; // so set it explicitly to the original parent ribbons datacontext
}
}
// store tracking list in hashtable using string key (= the view type name)
var key = rb.GetType().Name;
RibbonTabs[key] = tabList;
} else if (element is RibbonTab) {
// the datacontext should already be set in these circumstances
regionTarget.Items.Add(element);
}
}
break;
case NotifyCollectionChangedAction.Remove:
foreach (UIElement elementLoopVariable in e.OldItems) {
var element = elementLoopVariable;
if (element is Ribbon) {
Ribbon rb = element as Ribbon;
var key = rb.GetType().Name;
if (!RibbonTabs.ContainsKey(key)) continue; // no ribbon tabs have been tracked
var tabList = (RibbonTabs[key] as List<RibbonTab>) ?? new List<RibbonTab>();
foreach (RibbonTab rt in tabList)
{
if (!regionTarget.Items.Contains(rt)) continue; // this shouldn't happen
regionTarget.Items.Remove(rt); // remove from target region ribbon
rb.Items.Add(rt); // restore to view ribbon
}
RibbonTabs.Remove(key); // finished tracking so remove from hashtable
} else if (regionTarget.Items.Contains(element)) {
regionTarget.Items.Remove(element);
}
}
break;
}
};
}
protected override IRegion CreateRegion() {
return new SingleActiveRegion();
}
}

How to horizontally scroll in WPF using mouse tilt wheel?

How do you enable WPF to respond to horizontal scrolling using the mouse tilt wheel? For example, I have a Microsoft Explorer mini mouse and have tried horizontally scrolling content contained within a ScrollViewer with
HorizontalScrollBarVisibility="Visible"
but the content will not scroll horizontally. Vertical scrolling, however, works reliably as usual.
If such input is not directly supported by WPF at this time, is there a way to do this using interop with unmanaged code?
Thanks!
I just made a class that adds the PreviewMouseHorizontalWheel and MouseHorizontalWheel attached events to all UIElements.
These events include as parameter a MouseHorizontalWheelEventArgs HorizontalDelta.
Update 3
The tilt value was reversed according to WPF standards, where up is positive and down is negative, so made left positive and right negative.
Update 2
If the AutoEnableMouseHorizontalWheelSupport is set to true (as it is by default) there's no special requirement to use those events.
Only if it is set to false then you will need to call either MouseHorizontalWheelEnabler.EnableMouseHorizontalWheel(X)
where X is the top level element (Window, Popup or ContextMenu) or MouseHorizontalWheelEnabler.EnableMouseHorizontalWheelForParentOf(X) with the Element to enable support for. You can read the provided docs for more info on those methods.
Note that all this does nothing on XP, since WM_MOUSE-H-WHEEL was added on Vista.
MouseHorizontalWheelEnabler.cs
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Interop;
using JetBrains.Annotations;
namespace WpfExtensions
{
public static class MouseHorizontalWheelEnabler
{
/// <summary>
/// When true it will try to enable Horizontal Wheel support on parent windows/popups/context menus automatically
/// so the programmer does not need to call it.
/// Defaults to true.
/// </summary>
public static bool AutoEnableMouseHorizontalWheelSupport = true;
private static readonly HashSet<IntPtr> _HookedWindows = new HashSet<IntPtr>();
/// <summary>
/// Enable Horizontal Wheel support for all the controls inside the window.
/// This method does not need to be called if AutoEnableMouseHorizontalWheelSupport is true.
/// This does not include popups or context menus.
/// If it was already enabled it will do nothing.
/// </summary>
/// <param name="window">Window to enable support for.</param>
public static void EnableMouseHorizontalWheelSupport([NotNull] Window window) {
if (window == null) {
throw new ArgumentNullException(nameof(window));
}
if (window.IsLoaded) {
// handle should be available at this level
IntPtr handle = new WindowInteropHelper(window).Handle;
EnableMouseHorizontalWheelSupport(handle);
}
else {
window.Loaded += (sender, args) => {
IntPtr handle = new WindowInteropHelper(window).Handle;
EnableMouseHorizontalWheelSupport(handle);
};
}
}
/// <summary>
/// Enable Horizontal Wheel support for all the controls inside the popup.
/// This method does not need to be called if AutoEnableMouseHorizontalWheelSupport is true.
/// This does not include sub-popups or context menus.
/// If it was already enabled it will do nothing.
/// </summary>
/// <param name="popup">Popup to enable support for.</param>
public static void EnableMouseHorizontalWheelSupport([NotNull] Popup popup) {
if (popup == null) {
throw new ArgumentNullException(nameof(popup));
}
if (popup.IsOpen) {
// handle should be available at this level
// ReSharper disable once PossibleInvalidOperationException
EnableMouseHorizontalWheelSupport(GetObjectParentHandle(popup.Child).Value);
}
// also hook for IsOpened since a new window is created each time
popup.Opened += (sender, args) => {
// ReSharper disable once PossibleInvalidOperationException
EnableMouseHorizontalWheelSupport(GetObjectParentHandle(popup.Child).Value);
};
}
/// <summary>
/// Enable Horizontal Wheel support for all the controls inside the context menu.
/// This method does not need to be called if AutoEnableMouseHorizontalWheelSupport is true.
/// This does not include popups or sub-context menus.
/// If it was already enabled it will do nothing.
/// </summary>
/// <param name="contextMenu">Context menu to enable support for.</param>
public static void EnableMouseHorizontalWheelSupport([NotNull] ContextMenu contextMenu) {
if (contextMenu == null) {
throw new ArgumentNullException(nameof(contextMenu));
}
if (contextMenu.IsOpen) {
// handle should be available at this level
// ReSharper disable once PossibleInvalidOperationException
EnableMouseHorizontalWheelSupport(GetObjectParentHandle(contextMenu).Value);
}
// also hook for IsOpened since a new window is created each time
contextMenu.Opened += (sender, args) => {
// ReSharper disable once PossibleInvalidOperationException
EnableMouseHorizontalWheelSupport(GetObjectParentHandle(contextMenu).Value);
};
}
private static IntPtr? GetObjectParentHandle([NotNull] DependencyObject depObj) {
if (depObj == null) {
throw new ArgumentNullException(nameof(depObj));
}
var presentationSource = PresentationSource.FromDependencyObject(depObj) as HwndSource;
return presentationSource?.Handle;
}
/// <summary>
/// Enable Horizontal Wheel support for all the controls inside the HWND.
/// This method does not need to be called if AutoEnableMouseHorizontalWheelSupport is true.
/// This does not include popups or sub-context menus.
/// If it was already enabled it will do nothing.
/// </summary>
/// <param name="handle">HWND handle to enable support for.</param>
/// <returns>True if it was enabled or already enabled, false if it couldn't be enabled.</returns>
public static bool EnableMouseHorizontalWheelSupport(IntPtr handle) {
if (_HookedWindows.Contains(handle)) {
return true;
}
_HookedWindows.Add(handle);
HwndSource source = HwndSource.FromHwnd(handle);
if (source == null) {
return false;
}
source.AddHook(WndProcHook);
return true;
}
/// <summary>
/// Disable Horizontal Wheel support for all the controls inside the HWND.
/// This method does not need to be called in most cases.
/// This does not include popups or sub-context menus.
/// If it was already disabled it will do nothing.
/// </summary>
/// <param name="handle">HWND handle to disable support for.</param>
/// <returns>True if it was disabled or already disabled, false if it couldn't be disabled.</returns>
public static bool DisableMouseHorizontalWheelSupport(IntPtr handle) {
if (!_HookedWindows.Contains(handle)) {
return true;
}
HwndSource source = HwndSource.FromHwnd(handle);
if (source == null) {
return false;
}
source.RemoveHook(WndProcHook);
_HookedWindows.Remove(handle);
return true;
}
/// <summary>
/// Disable Horizontal Wheel support for all the controls inside the window.
/// This method does not need to be called in most cases.
/// This does not include popups or sub-context menus.
/// If it was already disabled it will do nothing.
/// </summary>
/// <param name="window">Window to disable support for.</param>
/// <returns>True if it was disabled or already disabled, false if it couldn't be disabled.</returns>
public static bool DisableMouseHorizontalWheelSupport([NotNull] Window window) {
if (window == null) {
throw new ArgumentNullException(nameof(window));
}
IntPtr handle = new WindowInteropHelper(window).Handle;
return DisableMouseHorizontalWheelSupport(handle);
}
/// <summary>
/// Disable Horizontal Wheel support for all the controls inside the popup.
/// This method does not need to be called in most cases.
/// This does not include popups or sub-context menus.
/// If it was already disabled it will do nothing.
/// </summary>
/// <param name="popup">Popup to disable support for.</param>
/// <returns>True if it was disabled or already disabled, false if it couldn't be disabled.</returns>
public static bool DisableMouseHorizontalWheelSupport([NotNull] Popup popup) {
if (popup == null) {
throw new ArgumentNullException(nameof(popup));
}
IntPtr? handle = GetObjectParentHandle(popup.Child);
if (handle == null) {
return false;
}
return DisableMouseHorizontalWheelSupport(handle.Value);
}
/// <summary>
/// Disable Horizontal Wheel support for all the controls inside the context menu.
/// This method does not need to be called in most cases.
/// This does not include popups or sub-context menus.
/// If it was already disabled it will do nothing.
/// </summary>
/// <param name="contextMenu">Context menu to disable support for.</param>
/// <returns>True if it was disabled or already disabled, false if it couldn't be disabled.</returns>
public static bool DisableMouseHorizontalWheelSupport([NotNull] ContextMenu contextMenu) {
if (contextMenu == null) {
throw new ArgumentNullException(nameof(contextMenu));
}
IntPtr? handle = GetObjectParentHandle(contextMenu);
if (handle == null) {
return false;
}
return DisableMouseHorizontalWheelSupport(handle.Value);
}
/// <summary>
/// Enable Horizontal Wheel support for all that control and all controls hosted by the same window/popup/context menu.
/// This method does not need to be called if AutoEnableMouseHorizontalWheelSupport is true.
/// If it was already enabled it will do nothing.
/// </summary>
/// <param name="uiElement">UI Element to enable support for.</param>
public static void EnableMouseHorizontalWheelSupportForParentOf(UIElement uiElement) {
// try to add it right now
if (uiElement is Window) {
EnableMouseHorizontalWheelSupport((Window)uiElement);
}
else if (uiElement is Popup) {
EnableMouseHorizontalWheelSupport((Popup)uiElement);
}
else if (uiElement is ContextMenu) {
EnableMouseHorizontalWheelSupport((ContextMenu)uiElement);
}
else {
IntPtr? parentHandle = GetObjectParentHandle(uiElement);
if (parentHandle != null) {
EnableMouseHorizontalWheelSupport(parentHandle.Value);
}
// and in the rare case the parent window ever changes...
PresentationSource.AddSourceChangedHandler(uiElement, PresenationSourceChangedHandler);
}
}
private static void PresenationSourceChangedHandler(object sender, SourceChangedEventArgs sourceChangedEventArgs) {
var src = sourceChangedEventArgs.NewSource as HwndSource;
if (src != null) {
EnableMouseHorizontalWheelSupport(src.Handle);
}
}
private static void HandleMouseHorizontalWheel(IntPtr wParam) {
int tilt = -Win32.HiWord(wParam);
if (tilt == 0) {
return;
}
IInputElement element = Mouse.DirectlyOver;
if (element == null) {
return;
}
if (!(element is UIElement)) {
element = VisualTreeHelpers.FindAncestor<UIElement>(element as DependencyObject);
}
if (element == null) {
return;
}
var ev = new MouseHorizontalWheelEventArgs(Mouse.PrimaryDevice, Environment.TickCount, tilt) {
RoutedEvent = PreviewMouseHorizontalWheelEvent
//Source = handledWindow
};
// first raise preview
element.RaiseEvent(ev);
if (ev.Handled) {
return;
}
// then bubble it
ev.RoutedEvent = MouseHorizontalWheelEvent;
element.RaiseEvent(ev);
}
private static IntPtr WndProcHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) {
// transform horizontal mouse wheel messages
switch (msg) {
case Win32.WM_MOUSEHWHEEL:
HandleMouseHorizontalWheel(wParam);
break;
}
return IntPtr.Zero;
}
private static class Win32
{
// ReSharper disable InconsistentNaming
public const int WM_MOUSEHWHEEL = 0x020E;
// ReSharper restore InconsistentNaming
public static int GetIntUnchecked(IntPtr value) {
return IntPtr.Size == 8 ? unchecked((int)value.ToInt64()) : value.ToInt32();
}
public static int HiWord(IntPtr ptr) {
return unchecked((short)((uint)GetIntUnchecked(ptr) >> 16));
}
}
#region MouseWheelHorizontal Event
public static readonly RoutedEvent MouseHorizontalWheelEvent =
EventManager.RegisterRoutedEvent("MouseHorizontalWheel", RoutingStrategy.Bubble, typeof(RoutedEventHandler),
typeof(MouseHorizontalWheelEnabler));
public static void AddMouseHorizontalWheelHandler(DependencyObject d, RoutedEventHandler handler) {
var uie = d as UIElement;
if (uie != null) {
uie.AddHandler(MouseHorizontalWheelEvent, handler);
if (AutoEnableMouseHorizontalWheelSupport) {
EnableMouseHorizontalWheelSupportForParentOf(uie);
}
}
}
public static void RemoveMouseHorizontalWheelHandler(DependencyObject d, RoutedEventHandler handler) {
var uie = d as UIElement;
uie?.RemoveHandler(MouseHorizontalWheelEvent, handler);
}
#endregion
#region PreviewMouseWheelHorizontal Event
public static readonly RoutedEvent PreviewMouseHorizontalWheelEvent =
EventManager.RegisterRoutedEvent("PreviewMouseHorizontalWheel", RoutingStrategy.Tunnel, typeof(RoutedEventHandler),
typeof(MouseHorizontalWheelEnabler));
public static void AddPreviewMouseHorizontalWheelHandler(DependencyObject d, RoutedEventHandler handler) {
var uie = d as UIElement;
if (uie != null) {
uie.AddHandler(PreviewMouseHorizontalWheelEvent, handler);
if (AutoEnableMouseHorizontalWheelSupport) {
EnableMouseHorizontalWheelSupportForParentOf(uie);
}
}
}
public static void RemovePreviewMouseHorizontalWheelHandler(DependencyObject d, RoutedEventHandler handler) {
var uie = d as UIElement;
uie?.RemoveHandler(PreviewMouseHorizontalWheelEvent, handler);
}
#endregion
}
}
MouseHorizontalWheelEventArgs.cs
using System.Windows.Input;
namespace WpfExtensions
{
public class MouseHorizontalWheelEventArgs : MouseEventArgs
{
public int HorizontalDelta { get; }
public MouseHorizontalWheelEventArgs(MouseDevice mouse, int timestamp, int horizontalDelta)
: base(mouse, timestamp) {
HorizontalDelta = horizontalDelta;
}
}
}
As for VisualTreeHelpers.FindAncestor, it is defined as follows:
/// <summary>
/// Returns the first ancestor of specified type
/// </summary>
public static T FindAncestor<T>(DependencyObject current) where T : DependencyObject {
current = GetVisualOrLogicalParent(current);
while (current != null) {
if (current is T) {
return (T)current;
}
current = GetVisualOrLogicalParent(current);
}
return null;
}
private static DependencyObject GetVisualOrLogicalParent(DependencyObject obj) {
if (obj is Visual || obj is Visual3D) {
return VisualTreeHelper.GetParent(obj);
}
return LogicalTreeHelper.GetParent(obj);
}
Call the AddHook() method in your Window constructor so you can spy on the messages. Look for WM_MOUSEHWHEEL, message 0x20e. Use wParam.ToInt32() >> 16 to get the movement amount, a multiple of 120.
Another solution using Attached Properties. It works for any Control that is either a ScrollViewer or contains a ScrollViewer. It is a fairly simple solution, and most importantly it is very easy to re-use. What I've done with my project is set this attached property in Generic.xaml for DataGrid, ListBox, ListView and friends. This way, it always just works.
This will work with multiple scroll viewers in the same UI, and will apply to whichever one currently has the mouse over it.
Here's the code for the attached property, as well as a helper class
note: C#6 syntax
Needed code
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interop;
namespace MyTestProject
{
public class TiltWheelHorizontalScroller
{
public static bool GetEnableTiltWheelScroll(DependencyObject obj) => (bool)obj.GetValue(EnableTiltWheelScrollProperty);
public static void SetEnableTiltWheelScroll(DependencyObject obj, bool value) => obj.SetValue(EnableTiltWheelScrollProperty, value);
public static readonly DependencyProperty EnableTiltWheelScrollProperty =
DependencyProperty.RegisterAttached("EnableTiltWheelScroll", typeof(bool), typeof(TiltWheelHorizontalScroller), new UIPropertyMetadata(false, OnHorizontalMouseWheelScrollingEnabledChanged));
static HashSet<int> controls = new HashSet<int>();
static void OnHorizontalMouseWheelScrollingEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs args)
{
Control control = d as Control;
if (control != null && GetEnableTiltWheelScroll(d) && controls.Add(control.GetHashCode()))
{
control.MouseEnter += (sender, e) =>
{
var scrollViewer = d.FindChildOfType<ScrollViewer>();
if (scrollViewer != null)
{
new TiltWheelMouseScrollHelper(scrollViewer, d);
}
};
}
}
}
class TiltWheelMouseScrollHelper
{
/// <summary>
/// multiplier of how far to scroll horizontally. Change as desired.
/// </summary>
private const int scrollFactor = 3;
private const int WM_MOUSEHWEEL = 0x20e;
ScrollViewer scrollViewer;
HwndSource hwndSource;
HwndSourceHook hook;
static HashSet<int> scrollViewers = new HashSet<int>();
public TiltWheelMouseScrollHelper(ScrollViewer scrollViewer, DependencyObject d)
{
this.scrollViewer = scrollViewer;
hwndSource = PresentationSource.FromDependencyObject(d) as HwndSource;
hook = WindowProc;
hwndSource?.AddHook(hook);
if (scrollViewers.Add(scrollViewer.GetHashCode()))
{
scrollViewer.MouseLeave += (sender, e) =>
{
hwndSource.RemoveHook(hook);
};
}
}
IntPtr WindowProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
switch (msg)
{
case WM_MOUSEHWEEL:
Scroll(wParam);
handled = true;
break;
}
return IntPtr.Zero;
}
private void Scroll(IntPtr wParam)
{
int delta = (HIWORD(wParam) > 0 ? 1 : -1) * scrollFactor;
scrollViewer.ScrollToHorizontalOffset(scrollViewer.HorizontalOffset + delta);
}
private static int HIWORD(IntPtr ptr) => (short)((((int)ptr.ToInt64()) >> 16) & 0xFFFF);
}
}
And you will need this extension method if you don't already have it.
/// <summary>
/// Finds first child of provided type. If child not found, null is returned
/// </summary>
/// <typeparam name="T">Type of chiled to be found</typeparam>
/// <param name="source"></param>
/// <returns></returns>
public static T FindChildOfType<T>(this DependencyObject originalSource) where T : DependencyObject
{
T ret = originalSource as T;
DependencyObject child = null;
if (originalSource != null && ret == null)
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(originalSource); i++)
{
child = VisualTreeHelper.GetChild(originalSource, i);
if (child != null)
{
if (child is T)
{
ret = child as T;
break;
}
else
{
ret = child.FindChildOfType<T>();
if (ret != null)
{
break;
}
}
}
}
}
return ret;
}
Usage
Simple example of a Window with a DataGrid. Here DataItems is just some fake data I made up for the test case.
<Window x:Class="MyTestProject.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ap="clr-namespace:MyTestProject"
Title="MainWindow" Height="350" Width="525"
DataContext="{Binding RelativeSource={RelativeSource Self}}">
<Grid>
<DataGrid x:Name="dataGrid"
ItemsSource="{Binding DataItems}"
ap:TiltWheelHorizontalScroller.EnableTiltWheelScroll="True"/>
</Grid>
</Window>
Or, what I ended up doing, put this style in Generic.xaml, or your Window.Resources to apply to all datagrids. You can attach this property to any control that has a ScrollViewer in it (and of course that horizontal scrolling is not disabled).
<Style TargetType="{x:Type DataGrid}" BasedOn="{StaticResource {x:Type DataGrid}}">
<Setter Property="ap:TiltWheelHorizontalScroller.EnableTiltWheelScroll" Value="True"/>
</Style>
T. Webster posted a WPF code snippet that adds horizontal mouse scroll support to any ScrollViewer and DependancyObject. It utilizes the AddHook and window messages as others have described.
I was able to adapt this to a behavior pretty quickly and attach it to a ScrollViewer in XAML.
This Microsoft link provides your exact requirement:
horizontal scrolling
and then just override this method -
private void OnMouseTilt(int tilt)
{
// Write your horizontal handling codes here.
if(!mainScrollViewer.IsVisible) return;
if (tilt > 0)
{
mainScrollViewer.LineLeft();
}
else
{
mainScrollViewer.LineRight();
}
}

How to expand all nodes of a WPF treeview in code behind?

I might be suffering of Monday's dumbness, but I can't find a nice way of expanding all treeview nodes after I've added them in code behind (something like treeView.ExpandAll()).
Any quick help?
In xaml you could do it as follows :
<TreeView.ItemContainerStyle>
<Style TargetType="TreeViewItem">
<Setter Property="TreeViewItem.IsExpanded" Value="True"/>
</Style>
</TreeView.ItemContainerStyle>
After playing around with all of the various methods for fully expanding and collapsing a tree view, by far the fastest method is the following. This method seems to work on very large trees.
Ensure your tree is virtualized, if it isn't virtualized then as soon as the tree gets to any kind of size it is going to become painfully slow whatever you do.
VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingStackPanel.VirtualizationMode="Recycling"
Assume that you have a view model backing your tree, each node on that view model that corresponds to a HierarchicalDataTemplate needs an IsExpanded property (it doesn't need to implement property changed). Assume these view models implement an interface like this:
interface IExpandableItem : IEnumerable
{
bool IsExpanded { get; set; }
}
The TreeViewItem style needs to be set as follows to bind the IsExpanded property in the view model to the view:
<Style
TargetType="{x:Type TreeViewItem}">
<Setter
Property="IsExpanded"
Value="{Binding
IsExpanded,
Mode=TwoWay}" />
</Style>
We are going to use this property to set the expansion state, but also, because the tree is virtualized this property is necessary to maintain the correct view state as the individual TreeViewItems get recycled. Without this binding nodes will get collapsed as they go out of view as the user browses the tree.
The only way to get acceptable speed on large trees is to work in code behind in the view layer. The plan is basically as follows:
Get hold of the current binding to the TreeView.ItemsSource.
Clear that binding.
Wait for the binding to actually clear.
Set the expansion state in the (now unbound) view model.
Rebind the TreeView.ItemsSource using the binding we cached in step 1.
Because we have virtualization enabled, performing a bind on TreeView.ItemsSource turns out to be very fast, even with a large view model. Likewise, when unbound updating the expansion state of the nodes should be very fast. This results in surprisingly fast updates.
Here is some code:
void SetExpandedStateInView(bool isExpanded)
{
var model = this.DataContext as TreeViewModel;
if (model == null)
{
// View model is not bound so do nothing.
return;
}
// Grab hold of the current ItemsSource binding.
var bindingExpression = this.TreeView.GetBindingExpression(
ItemsControl.ItemsSourceProperty);
if (bindingExpression == null)
{
return;
}
// Clear that binding.
var itemsSourceBinding = bindingExpression.ParentBinding;
BindingOperations.ClearBinding(
this.TreeView, ItemsControl.ItemsSourceProperty);
// Wait for the binding to clear and then set the expanded state of the view model.
this.Dispatcher.BeginInvoke(
DispatcherPriority.DataBind,
new Action(() => SetExpandedStateInModel(model.Items, isExpanded)));
// Now rebind the ItemsSource.
this.Dispatcher.BeginInvoke(
DispatcherPriority.DataBind,
new Action(
() => this.TreeView.SetBinding(
ItemsControl.ItemsSourceProperty, itemsSourceBinding)));
}
void SetExpandedStateInModel(IEnumerable modelItems, bool isExpanded)
{
if (modelItems == null)
{
return;
}
foreach (var modelItem in modelItems)
{
var expandable = modelItem as IExpandableItem;
if (expandable == null)
{
continue;
}
expandable.IsExpanded = isExpanded;
SetExpandedStateInModel(expandable, isExpanded);
}
}
WPF doesn't have an ExpandAll method. You'll need to loop through and set the property on each node.
See this question or this blog post.
I have done an ExpandAll that works also if your tree is set for virtualization (recycling items).
This is my code. Perhaps you should consider wrapping your hierarchy into a hierarchical model model view ?
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Threading;
using HQ.Util.General;
namespace HQ.Util.Wpf.WpfUtil
{
public static class TreeViewExtensions
{
// ******************************************************************
public delegate void OnTreeViewVisible(TreeViewItem tvi);
public delegate void OnItemExpanded(TreeViewItem tvi, object item);
public delegate void OnAllItemExpanded();
// ******************************************************************
private static void SetItemHierarchyVisible(ItemContainerGenerator icg, IList listOfRootToNodeItemPath, OnTreeViewVisible onTreeViewVisible = null)
{
Debug.Assert(icg != null);
if (icg != null)
{
if (listOfRootToNodeItemPath.Count == 0) // nothing to do
return;
TreeViewItem tvi = icg.ContainerFromItem(listOfRootToNodeItemPath[0]) as TreeViewItem;
if (tvi != null) // Due to threading, always better to verify
{
listOfRootToNodeItemPath.RemoveAt(0);
if (listOfRootToNodeItemPath.Count == 0)
{
if (onTreeViewVisible != null)
onTreeViewVisible(tvi);
}
else
{
if (!tvi.IsExpanded)
tvi.IsExpanded = true;
SetItemHierarchyVisible(tvi.ItemContainerGenerator, listOfRootToNodeItemPath, onTreeViewVisible);
}
}
else
{
ActionHolder actionHolder = new ActionHolder();
EventHandler itemCreated = delegate(object sender, EventArgs eventArgs)
{
var icgSender = sender as ItemContainerGenerator;
tvi = icgSender.ContainerFromItem(listOfRootToNodeItemPath[0]) as TreeViewItem;
if (tvi != null) // Due to threading, it is always better to verify
{
SetItemHierarchyVisible(icg, listOfRootToNodeItemPath, onTreeViewVisible);
actionHolder.Execute();
}
};
actionHolder.Action = new Action(() => icg.StatusChanged -= itemCreated);
icg.StatusChanged += itemCreated;
return;
}
}
}
// ******************************************************************
/// <summary>
/// You cannot rely on this method to be synchronous. If you have any action that depend on the TreeViewItem
/// (last item of collectionOfRootToNodePath) to be visible, you should set it in the 'onTreeViewItemVisible' method.
/// This method should work for Virtualized and non virtualized tree.
/// The difference with ExpandItem is that this one open up the tree up to the target but will not expand the target itself,
/// while ExpandItem expand the target itself.
/// </summary>
/// <param name="treeView">TreeView where an item has to be set visible</param>
/// <param name="listOfRootToNodePath">Any collectionic List. The collection should have every objet of the path to the targeted item from the root
/// to the target. For example for an apple tree: AppleTree (index 0), Branch4, SubBranch3, Leaf2 (index 3)</param>
/// <param name="onTreeViewVisible">Optionnal</param>
public static void SetItemHierarchyVisible(this TreeView treeView, IEnumerable<object> listOfRootToNodePath, OnTreeViewVisible onTreeViewVisible = null)
{
ItemContainerGenerator icg = treeView.ItemContainerGenerator;
if (icg == null)
return; // Is tree loaded and initialized ???
SetItemHierarchyVisible(icg, new List<object>(listOfRootToNodePath), onTreeViewVisible);
}
// ******************************************************************
private static void ExpandItem(ItemContainerGenerator icg, IList listOfRootToNodePath, OnTreeViewVisible onTreeViewVisible = null)
{
Debug.Assert(icg != null);
if (icg != null)
{
if (listOfRootToNodePath.Count == 0) // nothing to do
return;
TreeViewItem tvi = icg.ContainerFromItem(listOfRootToNodePath[0]) as TreeViewItem;
if (tvi != null) // Due to threading, always better to verify
{
listOfRootToNodePath.RemoveAt(0);
if (!tvi.IsExpanded)
tvi.IsExpanded = true;
if (listOfRootToNodePath.Count == 0)
{
if (onTreeViewVisible != null)
onTreeViewVisible(tvi);
}
else
{
SetItemHierarchyVisible(tvi.ItemContainerGenerator, listOfRootToNodePath, onTreeViewVisible);
}
}
else
{
ActionHolder actionHolder = new ActionHolder();
EventHandler itemCreated = delegate(object sender, EventArgs eventArgs)
{
var icgSender = sender as ItemContainerGenerator;
tvi = icgSender.ContainerFromItem(listOfRootToNodePath[0]) as TreeViewItem;
if (tvi != null) // Due to threading, it is always better to verify
{
SetItemHierarchyVisible(icg, listOfRootToNodePath, onTreeViewVisible);
actionHolder.Execute();
}
};
actionHolder.Action = new Action(() => icg.StatusChanged -= itemCreated);
icg.StatusChanged += itemCreated;
return;
}
}
}
// ******************************************************************
/// <summary>
/// You cannot rely on this method to be synchronous. If you have any action that depend on the TreeViewItem
/// (last item of collectionOfRootToNodePath) to be visible, you should set it in the 'onTreeViewItemVisible' method.
/// This method should work for Virtualized and non virtualized tree.
/// The difference with SetItemHierarchyVisible is that this one open the target while SetItemHierarchyVisible does not try to expand the target.
/// (SetItemHierarchyVisible just ensure the target will be visible)
/// </summary>
/// <param name="treeView">TreeView where an item has to be set visible</param>
/// <param name="listOfRootToNodePath">The collection should have every objet of the path, from the root to the targeted item.
/// For example for an apple tree: AppleTree (index 0), Branch4, SubBranch3, Leaf2</param>
/// <param name="onTreeViewVisible">Optionnal</param>
public static void ExpandItem(this TreeView treeView, IEnumerable<object> listOfRootToNodePath, OnTreeViewVisible onTreeViewVisible = null)
{
ItemContainerGenerator icg = treeView.ItemContainerGenerator;
if (icg == null)
return; // Is tree loaded and initialized ???
ExpandItem(icg, new List<object>(listOfRootToNodePath), onTreeViewVisible);
}
// ******************************************************************
private static void ExpandSubWithContainersGenerated(ItemsControl ic, Action<TreeViewItem, object> actionItemExpanded, ReferenceCounterTracker referenceCounterTracker)
{
ItemContainerGenerator icg = ic.ItemContainerGenerator;
foreach (object item in ic.Items)
{
var tvi = icg.ContainerFromItem(item) as TreeViewItem;
actionItemExpanded(tvi, item);
tvi.IsExpanded = true;
ExpandSubContainers(tvi, actionItemExpanded, referenceCounterTracker);
}
}
// ******************************************************************
/// <summary>
/// Expand any ItemsControl (TreeView, TreeViewItem, ListBox, ComboBox, ...) and their childs if any (TreeView)
/// </summary>
/// <param name="ic"></param>
/// <param name="actionItemExpanded"></param>
/// <param name="referenceCounterTracker"></param>
public static void ExpandSubContainers(ItemsControl ic, Action<TreeViewItem, object> actionItemExpanded, ReferenceCounterTracker referenceCounterTracker)
{
ItemContainerGenerator icg = ic.ItemContainerGenerator;
{
if (icg.Status == GeneratorStatus.ContainersGenerated)
{
ExpandSubWithContainersGenerated(ic, actionItemExpanded, referenceCounterTracker);
}
else if (icg.Status == GeneratorStatus.NotStarted)
{
ActionHolder actionHolder = new ActionHolder();
EventHandler itemCreated = delegate(object sender, EventArgs eventArgs)
{
var icgSender = sender as ItemContainerGenerator;
if (icgSender.Status == GeneratorStatus.ContainersGenerated)
{
ExpandSubWithContainersGenerated(ic, actionItemExpanded, referenceCounterTracker);
// Never use the following method in BeginInvoke due to ICG recycling. The same icg could be
// used and will keep more than one subscribers which is far from being intended
// ic.Dispatcher.BeginInvoke(actionHolder.Action, DispatcherPriority.Background);
// Very important to unsubscribe as soon we've done due to ICG recycling.
actionHolder.Execute();
referenceCounterTracker.ReleaseRef();
}
};
referenceCounterTracker.AddRef();
actionHolder.Action = new Action(() => icg.StatusChanged -= itemCreated);
icg.StatusChanged += itemCreated;
// Next block is only intended to protect against any race condition (I don't know if it is possible ? How Microsoft implemented it)
// I mean the status changed before I subscribe to StatusChanged but after I made the check about its state.
if (icg.Status == GeneratorStatus.ContainersGenerated)
{
ExpandSubWithContainersGenerated(ic, actionItemExpanded, referenceCounterTracker);
}
}
}
}
// ******************************************************************
/// <summary>
/// This method is asynchronous.
/// Expand all items and subs recursively if any. Does support virtualization (item recycling).
/// But honestly, make you a favor, make your life easier en create a model view around your hierarchy with
/// a IsExpanded property for each node level and bind it to each TreeView node level.
/// </summary>
/// <param name="treeView"></param>
/// <param name="actionItemExpanded"></param>
/// <param name="actionAllItemExpanded"></param>
public static void ExpandAll(this TreeView treeView, Action<TreeViewItem, object> actionItemExpanded = null, Action actionAllItemExpanded = null)
{
var referenceCounterTracker = new ReferenceCounterTracker(actionAllItemExpanded);
referenceCounterTracker.AddRef();
treeView.Dispatcher.BeginInvoke(new Action(() => ExpandSubContainers(treeView, actionItemExpanded, referenceCounterTracker)), DispatcherPriority.Background);
referenceCounterTracker.ReleaseRef();
}
// ******************************************************************
}
}
And
using System;
using System.Threading;
namespace HQ.Util.General
{
public class ReferenceCounterTracker
{
private Action _actionOnCountReachZero = null;
private int _count = 0;
public ReferenceCounterTracker(Action actionOnCountReachZero)
{
_actionOnCountReachZero = actionOnCountReachZero;
}
public void AddRef()
{
Interlocked.Increment(ref _count);
}
public void ReleaseRef()
{
int count = Interlocked.Decrement(ref _count);
if (count == 0)
{
if (_actionOnCountReachZero != null)
{
_actionOnCountReachZero();
}
}
}
}
}
You have to include the following method in your project:
private void ExpandAllNodes(TreeViewItem treeItem)
{
treeItem.IsExpanded = true;
foreach (var childItem in treeItem.Items.OfType<TreeViewItem>())
{
ExpandAllNodes(childItem);
}
}
then, you only need to call it like this:
treeView.Items.OfType<TreeViewItem>().ToList().ForEach(ExpandAllNodes);

Categories

Resources