How to prevent ScrollViewer firing ViewChanged event while it is updating? - c#

I have two ScrollViewer and I need to sync position of those ScrollViewer when any of the ScrollViewer gets changes, but right now suppose when anyone scrollviewer2 is changed then on calling ChangeView event of ScrollViewer1 it's firing its ViewChangedEvent which is resetting ScrollViewer2 position back.
private void Scroll(ScrollViewer changedScrollViewer)
{
var group = ScrollViewers[changedScrollViewer];
VerticalScrollOffsets[group] = changedScrollViewer.VerticalOffset;
HorizontalScrollOffsets[group] = changedScrollViewer.HorizontalOffset;
foreach (var scrollViewer in ScrollViewers.Where(s => s.Value == group && s.Key != changedScrollViewer))
{
scrollViewer.Key.ViewChanged -= ScrollViewer_ViewChanged;
if (scrollViewer.Key.VerticalOffset != changedScrollViewer.VerticalOffset)
{
scrollViewer.Key.ChangeView(null, changedScrollViewer.VerticalOffset, null, true);
}
if (scrollViewer.Key.HorizontalOffset != changedScrollViewer.HorizontalOffset)
{
scrollViewer.Key.ChangeView(changedScrollViewer.HorizontalOffset, null, null, true);
}
//Commenting this line works. But I need to set ViewChange event back.
scrollViewer.Key.ViewChanged += ScrollViewer_ViewChanged;
}
}

#Nico's solution is much preferable. If you still need something with a flag, it 'll look like this:
bool is_programmatic_call = false;
private void ScrollViewer_ViewChanged(object sender, ScrollViewerViewChangedEventArgs e)
{
if (is_programmatic_call)
{
is_programmatic_call = false;
return;
}
if(sender == ScrollViewer1)
{
ScrollViewer2.ViewChanged -= ScrollViewer_ViewChanged;
is_programmatic_call = true;
ScrollViewer2.ChangeView(ScrollViewer1.HorizontalOffset, ScrollViewer1.VerticalOffset, null, true);
ScrollViewer2.ViewChanged += ScrollViewer_ViewChanged;
}
else
{
ScrollViewer1.ViewChanged -= ScrollViewer_ViewChanged;
is_programmatic_call = true;
ScrollViewer1.ChangeView(ScrollViewer2.HorizontalOffset, ScrollViewer2.VerticalOffset, null, true);
ScrollViewer1.ViewChanged += ScrollViewer_ViewChanged;
}
}
both the ScrollViewer's ViewChanged event is handled by this ScrollViewer_ViewChanged

For sync two ScrollViewers, the better way is make a new Dependency Property, and bind it with the same value. It will notify the ScrollViewer to scroll automatically when the Dependency Property value changed. This solution will stop Circular Reference happening in the ViewChanged event.
I have implemented it for ListView in this code sample. You could refer segment code. But for ScrollViewer, you need to make xaml Behavior, because ScrollViewer is sealed class, it could not be inherited.
public class SyncBehavior : Behavior<ScrollViewer>
{
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.Loaded += OnAssociatedObjectLoaded;
AssociatedObject.LayoutUpdated += OnAssociatedObjectLayoutUpdated;
}
protected override void OnDetaching()
{
base.OnDetaching();
AssociatedObject.Loaded -= OnAssociatedObjectLoaded;
AssociatedObject.LayoutUpdated -= OnAssociatedObjectLayoutUpdated;
}
private void OnAssociatedObjectLayoutUpdated(object sender, object o)
{
SyncPointOffSetY();
}
private void OnAssociatedObjectLoaded(object sender, RoutedEventArgs routedEventArgs)
{
SyncPointOffSetY();
AssociatedObject.Loaded -= OnAssociatedObjectLoaded;
}
private void SyncPointOffSetY()
{
if (AssociatedObject == null) return;
AssociatedObject.ViewChanged += AssociatedObject_ViewChanged;
}
private void AssociatedObject_ViewChanged(object sender, ScrollViewerViewChangedEventArgs e)
{
var MyScrollViewer = sender as ScrollViewer;
this.SetValue(PointOffSetYProperty, MyScrollViewer.VerticalOffset);
}
public double PointOffSetY
{
get { return (double)GetValue(PointOffSetYProperty); }
set { SetValue(PointOffSetYProperty, value); }
}
// Using a DependencyProperty as the backing store for MyProperty. This enables animation, styling, binding, etc...
public static readonly DependencyProperty PointOffSetYProperty =
DependencyProperty.Register("PointOffSetY", typeof(double), typeof(SyncBehavior), new PropertyMetadata(0.0, CallBack));
private static void CallBack(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var current = d as SyncBehavior;
var temScrollViewer = current.AssociatedObject;
if (e.NewValue != e.OldValue & (double)e.NewValue != 0)
{
temScrollViewer.ScrollToVerticalOffset((double)e.NewValue);
}
}
}
Usage
<ScrollViewer >
<Interactivity:Interaction.Behaviors>
<local:SyncBehavior PointOffSetY="{Binding PointY,Mode=TwoWay}"/>
</Interactivity:Interaction.Behaviors>
<StackPanel >
<Rectangle Height="500" Fill="Red"/>
<Rectangle Height="500" Fill="Black"/>
<Rectangle Height="500" Fill="Yellow"/>
</StackPanel>
</ScrollViewer>
<ScrollViewer Grid.Column="1" >
<Interactivity:Interaction.Behaviors>
<local:SyncBehavior PointOffSetY="{Binding PointY,Mode=TwoWay}"/>
</Interactivity:Interaction.Behaviors>
<StackPanel >
<Rectangle Height="500" Fill="Red"/>
<Rectangle Height="500" Fill="Black"/>
<Rectangle Height="500" Fill="Yellow"/>
</StackPanel>
</ScrollViewer>
And I have also added the above code to the sample that you could refer easily.

Related

Selected item as Current Item in c#

I have a list box which is bound to a collection in C# WPF. When I search for a record I want to move the selected item to the top of the list and mark as selected.
Here is my code:
var loc = lst_sub.Items.IndexOf(name);
lst_sub.SelectedIndex = loc;
lst_sub.Items.MoveCurrentToFirst();
This can be handled using a Behavior class ...
public class perListBoxHelper : Behavior<ListBox>
{
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.SelectionChanged += AssociatedObject_SelectionChanged;
}
protected override void OnDetaching()
{
AssociatedObject.SelectionChanged -= AssociatedObject_SelectionChanged;
base.OnDetaching();
}
private static void AssociatedObject_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var listBox = sender as ListBox;
if (listBox?.SelectedItem == null)
{
return;
}
Action action = () =>
{
listBox.UpdateLayout();
if (listBox.SelectedItem == null)
{
return;
}
listBox.ScrollIntoView(listBox.SelectedItem);
};
listBox.Dispatcher.BeginInvoke(action, DispatcherPriority.ContextIdle);
}
}
Usage ...
<ListBox
Width="200"
Height="200"
ItemsSource="{Binding Items}"
SelectedItem="{Binding SelectedItem}">
<i:Interaction.Behaviors>
<vhelp:perListBoxHelper />
</i:Interaction.Behaviors>
</ListBox>
More details on my blog post.

Animate DataGridRow with Behavior

I need to animate a row in certain circumstances. I have a Behaivor that only runs when a certain property changes. The problem I have is that when the behaivor ends, I lose all the animations I had before. Watch the mouse over before and after the animation. Also, when finished, observe when I click on the row. Any solution?
.gif
public enum Events
{
Changed
}
public class AnimateBehavior : Behavior<TextBlock>
{
public Events Property { get; set; }
private ColorAnimation ColorAnimation { get; set; }
public Brush BackgroundBrush { get; set; }
protected override void OnAttached()
{
ColorAnimation = new ColorAnimation
{
AutoReverse = true,
To = Colors.LightBlue,
From = Colors.Transparent,
FillBehavior = FillBehavior.Stop,
RepeatBehavior = new RepeatBehavior(3),
Duration = new Duration(TimeSpan.FromMilliseconds(500))
};
AssociatedObject.DataContextChanged += OnDataContextChanged;
WireEvents();
}
private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if (e.OldValue is INotifyPropertyChanged oldData)
{
oldData.PropertyChanged -= OnPropertyChanged;
}
WireEvents();
}
private void WireEvents()
{
if (AssociatedObject.DataContext is INotifyPropertyChanged currentData)
{
currentData.PropertyChanged += OnPropertyChanged;
}
}
private void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (Property == Events.Changed)
{
var dataGridRow = AssociatedObject.TryFindParent<DataGridRow>();
//ColorAnimation.Completed += (s, x) =>
//{
// dataGridRow.Background = Brushes.Transparent;
//};
dataGridRow.Background =
(SolidColorBrush) new BrushConverter().ConvertFromString(dataGridRow.Background.ToString());
dataGridRow.Background?
.BeginAnimation(SolidColorBrush.ColorProperty, ColorAnimation);
}
}
protected override void OnDetaching()
{
if (AssociatedObject.DataContext is INotifyPropertyChanged currentData)
{
currentData.PropertyChanged -= OnPropertyChanged;
}
}
}
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding LastUpdated}">
<Interactivity:Interaction.Behaviors>
<customBehavior:AnimateBehavior Property="Changed" />
</Interactivity:Interaction.Behaviors>
</TextBlock>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>

UWP CommandBar Binding

is it possible to bind an UWP CommandBar to something like a ObservableCollection or so?
What i want to achieve ist to bind my CommandBar of my NavigationView to an Object of a specific Page so that the AppBarButton change dynamicaly depending on the current Page
What i tryed:
MainPage.xaml
<NavigationView.HeaderTemplate>
<DataTemplate>
<Grid>
<CommandBar Grid.Column="1"
HorizontalAlignment="Right"
VerticalAlignment="Top"
DefaultLabelPosition="Right"
Background="{ThemeResource SystemControlBackgroundAltHighBrush}" Content="{Binding Path=Content.AppBarButtonList, ElementName=rootFrame}">
</CommandBar>
</Grid>
</DataTemplate>
</NavigationView.HeaderTemplate>
SomePage.xaml.cs
public ObservableCollection<AppBarButton> AppBarButtonList = new ObservableCollection<AppBarButton> {
new AppBarButton { Icon = new SymbolIcon(Symbol.Accept), Label="Bla" },
new AppBarButton{Icon=new SymbolIcon(Symbol.Add),Label="Add"}
};
But the CommandBar shows nothing.
Thanks.
My original solution was using the PrimaryCommands property to bind the commands, but it turns out this property is read-only.
My solution to the problem will be using behaviors.
First add a reference to Microsoft.Xaml.Behaviors.Uwp.Managed from NuGet.
Then add the following behavior to your project:
public class BindableCommandBarBehavior : Behavior<CommandBar>
{
public ObservableCollection<AppBarButton> PrimaryCommands
{
get { return (ObservableCollection<AppBarButton>)GetValue(PrimaryCommandsProperty); }
set { SetValue(PrimaryCommandsProperty, value); }
}
public static readonly DependencyProperty PrimaryCommandsProperty = DependencyProperty.Register(
"PrimaryCommands", typeof(ObservableCollection<AppBarButton>), typeof(BindableCommandBarBehavior), new PropertyMetadata(default(ObservableCollection<AppBarButton>), UpdateCommands));
private static void UpdateCommands(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
{
if (!(dependencyObject is BindableCommandBarBehavior behavior)) return;
var oldList = dependencyPropertyChangedEventArgs.OldValue as ObservableCollection<AppBarButton>;
if (dependencyPropertyChangedEventArgs.OldValue != null)
{
oldList.CollectionChanged -= behavior.PrimaryCommandsCollectionChanged;
}
var newList = dependencyPropertyChangedEventArgs.NewValue as ObservableCollection<AppBarButton>;
if (dependencyPropertyChangedEventArgs.NewValue != null)
{
newList.CollectionChanged += behavior.PrimaryCommandsCollectionChanged;
}
behavior.UpdatePrimaryCommands();
}
private void PrimaryCommandsCollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
UpdatePrimaryCommands();
}
private void UpdatePrimaryCommands()
{
if (PrimaryCommands != null)
{
AssociatedObject.PrimaryCommands.Clear();
foreach (var command in PrimaryCommands)
{
AssociatedObject.PrimaryCommands.Add(command);
}
}
}
protected override void OnDetaching()
{
base.OnDetaching();
if (PrimaryCommands != null)
{
PrimaryCommands.CollectionChanged -= PrimaryCommandsCollectionChanged;
}
}
}
This behavior essentially creates a fake PrimaryCommands property that is bindable and also observes collection changed events. Whenever a change occurs, the commands are rebuilt.
Finally, the problem in your code is that your AppBarButtonList is just a field, not a property. Change it like this:
public ObservableCollection<AppBarButton> AppBarButtonList { get; } = new ObservableCollection<AppBarButton> {
new AppBarButton { Icon = new SymbolIcon(Symbol.Accept), Label="Bla" },
new AppBarButton{Icon=new SymbolIcon(Symbol.Add),Label="Add"}
};
Notice the {get ;} which was added before the assignment operator.
Now you can use the behavior in XAML like this:
<CommandBar>
<interactivity:Interaction.Behaviors>
<local:BindableCommandBarBehavior PrimaryCommands="{Binding Path=Content.AppBarButtonList, ElementName=rootFrame}" />
</interactivity:Interaction.Behaviors>
</CommandBar>
This is by no means a perfect solution and could be improved upon to allow different collection types binding and more, but it should cover your scenario. An alternative solution would be to implement a custom version of command bar, with new additional dependency property directly on the type, but I used behavior to make it clearer for the user that this is an "added" functionality, not a built-in one.
I found this answer very helpful. I did some more adjustments, like using a DataTemplateSelector to remove UI references like "AppBarButton" from the bindable data source.
public class BindableCommandBarBehavior : Behavior<CommandBar>
{
public static readonly DependencyProperty PrimaryCommandsProperty = DependencyProperty.Register(
"PrimaryCommands", typeof(object), typeof(BindableCommandBarBehavior),
new PropertyMetadata(null, UpdateCommands));
public static readonly DependencyProperty ItemTemplateSelectorProperty = DependencyProperty.Register(
"ItemTemplateSelector", typeof(DataTemplateSelector), typeof(BindableCommandBarBehavior),
new PropertyMetadata(null, null));
public DataTemplateSelector ItemTemplateSelector
{
get { return (DataTemplateSelector)GetValue(ItemTemplateSelectorProperty); }
set { SetValue(ItemTemplateSelectorProperty, value); }
}
public object PrimaryCommands
{
get { return GetValue(PrimaryCommandsProperty); }
set { SetValue(PrimaryCommandsProperty, value); }
}
protected override void OnDetaching()
{
base.OnDetaching();
if (PrimaryCommands is INotifyCollectionChanged notifyCollectionChanged)
{
notifyCollectionChanged.CollectionChanged -= PrimaryCommandsCollectionChanged;
}
}
private void UpdatePrimaryCommands()
{
if (AssociatedObject == null)
return;
if (PrimaryCommands == null)
return;
AssociatedObject.PrimaryCommands.Clear();
if (!(PrimaryCommands is IEnumerable enumerable))
{
AssociatedObject.PrimaryCommands.Clear();
return;
}
foreach (var command in enumerable)
{
var template = ItemTemplateSelector.SelectTemplate(command, AssociatedObject);
if (!(template?.LoadContent() is FrameworkElement dependencyObject))
continue;
dependencyObject.DataContext = command;
if (dependencyObject is ICommandBarElement icommandBarElement)
AssociatedObject.PrimaryCommands.Add(icommandBarElement);
}
}
protected override void OnAttached()
{
base.OnAttached();
UpdatePrimaryCommands();
}
private void PrimaryCommandsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
UpdatePrimaryCommands();
}
private static void UpdateCommands(DependencyObject dependencyObject,
DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
{
if (!(dependencyObject is BindableCommandBarBehavior behavior)) return;
if (dependencyPropertyChangedEventArgs.OldValue is INotifyCollectionChanged oldList)
{
oldList.CollectionChanged -= behavior.PrimaryCommandsCollectionChanged;
}
if (dependencyPropertyChangedEventArgs.NewValue is INotifyCollectionChanged newList)
{
newList.CollectionChanged += behavior.PrimaryCommandsCollectionChanged;
}
behavior.UpdatePrimaryCommands();
}
}
The DataTemplateSelector:
public class CommandBarMenuItemTemplateSelector : DataTemplateSelector
{
public DataTemplate CbMenuItemTemplate { get; set; }
protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
{
if (item is ContextAction)
{
return CbMenuItemTemplate;
}
return base.SelectTemplateCore(item, container);
}
}
Xaml for templates:
<DataTemplate x:Key="CbMenuItemTemplate">
<AppBarButton
Command="{Binding Command}"
Icon="Add"
Label="{Binding Text}" />
</DataTemplate>
<viewLogic:CommandBarMenuItemTemplateSelector x:Key="CommandBarMenuItemTemplateSelector"
CbMenuItemTemplate="{StaticResource CbMenuItemTemplate}" />
Usage:
<CommandBar>
<interactivity:Interaction.Behaviors>
<viewLogic:BindableCommandBarBehavior ItemTemplateSelector="{StaticResource CommandBarMenuItemTemplateSelector}" PrimaryCommands="{Binding ContextActions}" />
</interactivity:Interaction.Behaviors>
</CommandBar>
Where ContextActions is a ObservableCollection of my class ContextAction.

Universally usable editable label

I'm implementing a way to rename elements in a treeview. My thought was not to build a special TreeView, but instead a special Label which can also be used in ListBox and other controls. It sets the focus to itself by clicking it, so I changed Focusable to true. If focused it gets editable by [F2] and when it gets clicked while already having focus. Editable means, a text box shows (all similar to Windows Explorer). Unfortunately the TreeViewItem does not get selected, when I click the label. When clicking the Icon to the left, the TreeViewItem is selected.
I do not handle any event setting Handled=true, but I think the Label itself handles the click, so the treeviewItem does not get the opportunity to react to it. If I am right here, is there a way to modify the label (own class?) in a way it does not handle the event at all?
I rethought my attempt. The solution is not to take the focus at all. Instead I bound a handler to the KeyUp event of the observed element currently having the focus (see the comment in EditableLabel.cs).
EditableLabel.xaml:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:myown.Wpf"
xmlns:localCtrls="clr-namespace:myown.Wpf.Controls">
<Style TargetType="{x:Type localCtrls:EditableLabel}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type localCtrls:EditableLabel}">
<StackPanel>
<Label x:Name="label"
Content="{TemplateBinding Text}"
Visibility="Visible"
Height="{TemplateBinding Height}"
Width="{TemplateBinding Width}"
Padding="{TemplateBinding Padding}"
Foreground="{TemplateBinding Foreground}"/>
<!-- TemplateBinding is always OneWay! -->
<TextBox x:Name="textbox"
Text="{Binding Path=Text, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
Visibility="Collapsed"
Height="{TemplateBinding Height}"
Width="{TemplateBinding Width}"
Padding="{TemplateBinding Padding}"/>
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
EditableLabel.cs:
public partial class EditableLabel : Label
{
#region DependencyProperties
public static readonly DependencyProperty IsEditingProperty =
DependencyProperty.Register("IsEditing", typeof(bool), typeof(EditableLabel),
new UIPropertyMetadata(false, IsEditingUpdate));
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register("Text", typeof(string), typeof(EditableLabel),
new FrameworkPropertyMetadata("",
FrameworkPropertyMetadataOptions.AffectsRender |
FrameworkPropertyMetadataOptions.AffectsParentMeasure |
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public bool IsEditing
{
get { return (bool)GetValue(IsEditingProperty); }
set { SetValue(IsEditingProperty, value); }
}
public string Text
{
get { return (string)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
private string m_lastValue = "";
private IInputElement m_lastFocus = null;
private Label m_label = null;
private TextBox m_textBox = null;
#endregion
static EditableLabel()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(EditableLabel),
new FrameworkPropertyMetadata(typeof(EditableLabel)));
}
public EditableLabel()
{
LostFocus += (s, e) => { };
LostKeyboardFocus += EditableLabel_LostKeyboardFocus;
}
private void EditableLabel_LostKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
{
if (e.NewFocus != null)
Debug.WriteLine(e.NewFocus.GetType().Name);
}
public override void OnApplyTemplate()
{
m_label = GetTemplateChild("label") as Label;
m_textBox = GetTemplateChild("textbox") as TextBox;
m_label.MouseUp += Label_MouseUp;
m_textBox.LostFocus += (s, e) => IsEditing = false;
m_textBox.KeyUp += (s, e) => { TextBox_KeyUp(s, e); e.Handled = true; };
m_textBox.GotKeyboardFocus += (s, e) => m_textBox.SelectAll();
base.OnApplyTemplate();
}
private void Label_MouseUp(object sender, MouseButtonEventArgs e)
{
DependencyObject scope = FocusManager.GetFocusScope(this);
IInputElement currFocus = FocusManager.GetFocusedElement(scope);
if ((currFocus != null))
{
if (currFocus == m_lastFocus)
{
IsEditing = true;
}
else
{
m_lastFocus = currFocus;
DependencyObject observedObject = currFocus as DependencyObject;
if (observedObject != null)
{
m_lastFocus.LostKeyboardFocus += ObservedElementLostFocus;
m_lastFocus.KeyUp += ObservedElement_KeyUp; // THIS IS THE TRICK
}
}
}
}
private void ObservedElement_KeyUp(object sender, KeyEventArgs e)
{
if (e.Key == Key.F2)
{
IsEditing = true;
e.Handled = true;
}
}
private void ObservedElementLostFocus(object sender, RoutedEventArgs e)
{
KeyboardFocusChangedEventArgs kfc = e as KeyboardFocusChangedEventArgs;
if (kfc.NewFocus != null)
{
if ((kfc.NewFocus != m_textBox) && (kfc.NewFocus != m_lastFocus))
{
m_lastFocus.LostKeyboardFocus -= ObservedElementLostFocus;
m_lastFocus.KeyUp -= ObservedElement_KeyUp;
m_lastFocus = null;
}
}
}
private void TextBox_KeyUp(object sender, KeyEventArgs e)
{
if (e.Key == Key.Escape)
{
IsEditing = false;
Text = m_lastValue;
}
else if (e.Key == Key.Enter)
{
IsEditing = false;
}
}
private static void IsEditingUpdate(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
EditableLabel self = obj as EditableLabel;
if ((bool)e.NewValue)
{
self.m_lastValue = self.Text;
self.m_textBox.Visibility = Visibility.Visible;
self.m_textBox.Focus();
self.m_label.Visibility = Visibility.Collapsed;
}
else
{
self.m_textBox.Visibility = Visibility.Collapsed;
self.m_label.Visibility = Visibility.Visible;
if (self.m_lastFocus != null)
self.m_lastFocus.Focus();
}
}
}

TreeView SelectedItem Behavior - Two Way Binding does not work in One Direction

I have created a very simple example to show my problem. Maybe I just think in a wrong way.
I want to select an Item of my TreeView - and I would like to see it in the View (Blue background).
To realize the TwoWayBinding I use this Behavior: Data binding to SelectedItem in a WPF Treeview
public class BindableSelectedItemBehavior : Behavior<TreeView>
{
#region SelectedItem Property
public object SelectedItem
{
get { return (object)GetValue(SelectedItemProperty); }
set { SetValue(SelectedItemProperty, value); }
}
public static readonly DependencyProperty SelectedItemProperty =
DependencyProperty.Register("SelectedItem", typeof(object), typeof(BindableSelectedItemBehavior), new UIPropertyMetadata(null, OnSelectedItemChanged));
private static void OnSelectedItemChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
var item = e.NewValue as TreeViewItem;
if (item != null)
{
item.SetValue(TreeViewItem.IsSelectedProperty, true);
}
}
#endregion
protected override void OnAttached()
{
base.OnAttached();
this.AssociatedObject.SelectedItemChanged += OnTreeViewSelectedItemChanged;
}
protected override void OnDetaching()
{
base.OnDetaching();
if (this.AssociatedObject != null)
{
this.AssociatedObject.SelectedItemChanged -= OnTreeViewSelectedItemChanged;
}
}
private void OnTreeViewSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
this.SelectedItem = e.NewValue;
}
}
But if I click on an Item it does not go into the 'if' of the OnSelectedItemChanged because e.newValue as TreeViewItem is null
My XAML is very simple:
<StackPanel>
<TreeView xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
ItemsSource="{Binding Items}">
<i:Interaction.Behaviors>
<local:BindableSelectedItemBehavior
SelectedItem="{Binding Item}" />
</i:Interaction.Behaviors>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate>
<TextBlock Text="{Binding Text}"/>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
<TextBox Text="{Binding Item.Text}"/>
</StackPanel>
Thank you guys!
Just for the sake of the convenience, here's the final solution combined of the OP and ghrod's answer:
namespace MyPoject.Behaviors
{
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interactivity;
public class BindableSelectedItemBehavior : Behavior<TreeView>
{
#region SelectedItem Property
public object SelectedItem
{
get { return (object)GetValue(SelectedItemProperty); }
set { SetValue(SelectedItemProperty, value); }
}
public static readonly DependencyProperty SelectedItemProperty =
DependencyProperty.Register(
nameof(SelectedItem),
typeof(object),
typeof(BindableSelectedItemBehavior),
new FrameworkPropertyMetadata(null,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
OnSelectedItemChanged));
static void OnSelectedItemChanged(DependencyObject sender,
DependencyPropertyChangedEventArgs e)
{
var behavior = (BindableSelectedItemBehavior)sender;
var generator = behavior.AssociatedObject.ItemContainerGenerator;
if (generator.ContainerFromItem(e.NewValue) is TreeViewItem item)
item.SetValue(TreeViewItem.IsSelectedProperty, true);
}
#endregion
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.SelectedItemChanged += OnTreeViewSelectedItemChanged;
}
protected override void OnDetaching()
{
base.OnDetaching();
if (this.AssociatedObject != null)
AssociatedObject.SelectedItemChanged -= OnTreeViewSelectedItemChanged;
}
void OnTreeViewSelectedItemChanged(object sender,
RoutedPropertyChangedEventArgs<object> e) =>
SelectedItem = e.NewValue;
}
}
SelectedItem property of TreeView does not return TreeViewItem in your case. It returns currently selected item from your bound Items collection. To get TreeViewItem from SelectedItem, you need to use ItemContainerGenerator here:
private static void OnSelectedItemChanged(DependencyObject sender,
DependencyPropertyChangedEventArgs e)
{
var behavior = (BindableSelectedItemBehavior)sender;
var generator = behavior.AssociatedObject.ItemContainerGenerator;
var item = generator.ContainerFromItem(e.NewValue) as TreeViewItem;
if (item != null)
{
item.SetValue(TreeViewItem.IsSelectedProperty, true);
}
}
Your OnSelectedItemChanged will only pass an object of type TreeViewItem if your actual model objects are of type TreeViewItem, which isn't going to be the case 99% of the time. You will instead have to retrieve TreeViewItem from a model object, but it will not be available if the node is currently collapsed, which makes selecting collapsed nodes very non-trivial.
I've made an effort to explain this very thoroughly in my blog, including code samples here

Categories

Resources