I have a treeview bound to an object tree. When I remove an object from the object tree, it is removed correctly from the tree view, but the treeview's default behaviour is to jump the selecteditem up to the deleted item's parent node. How can I change this so it jumps to the next item instead?
EDIT:
I updated my code with Aviad's suggestion. Here is my code..
public class ModifiedTreeView : TreeView
{
protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
{
base.OnItemsChanged(e);
if (e.Action == NotifyCollectionChangedAction.Remove)
{
if (e.OldStartingIndex - 1 > 0)
{
ModifiedTreeViewItem item =
this.ItemContainerGenerator.ContainerFromIndex(
e.OldStartingIndex - 2) as ModifiedTreeViewItem;
item.IsSelected = true;
}
}
}
protected override DependencyObject GetContainerForItemOverride()
{
return new ModifiedTreeViewItem();
}
protected override bool IsItemItsOwnContainerOverride(object item)
{
return item is ModifiedTreeViewItem;
}
}
public class ModifiedTreeViewItem : TreeViewItem
{
protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
{
base.OnItemsChanged(e);
if (e.Action == NotifyCollectionChangedAction.Remove)
{
if (e.OldStartingIndex > 0)
{
ModifiedTreeViewItem item =
this.ItemContainerGenerator.ContainerFromIndex(
e.OldStartingIndex - 1) as ModifiedTreeViewItem;
item.IsSelected = true;
}
}
}
protected override DependencyObject GetContainerForItemOverride()
{
return new ModifiedTreeViewItem();
}
protected override bool IsItemItsOwnContainerOverride(object item)
{
return item is ModifiedTreeViewItem;
}
}
The code above does not work unless I debug it, or in some way slow down the OnItemsChanged method. For example, if I put a thread.sleep(500) at the bottom of the OnItemsChanged method, it works, otherwise it does not. Any idea what I'm doing wrong? This is really strange.
The behavior you mention is controlled by a virtual method in the Selector class called OnItemsChanged (reference: Selector.OnItemsChanged Method) - In order to modify it, you should derive from TreeView and override that function. You might use reflector to base your implementation on the existing implementation, although it's pretty straightforward.
Here's the code for the treeview override TreeView.OnItemsChanged extracted using reflector:
protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
case NotifyCollectionChangedAction.Move:
break;
case NotifyCollectionChangedAction.Remove:
case NotifyCollectionChangedAction.Reset:
if ((this.SelectedItem == null) || this.IsSelectedContainerHookedUp)
{
break;
}
this.SelectFirstItem();
return;
case NotifyCollectionChangedAction.Replace:
{
object selectedItem = this.SelectedItem;
if ((selectedItem == null) || !selectedItem.Equals(e.OldItems[0]))
{
break;
}
this.ChangeSelection(selectedItem, this._selectedContainer, false);
return;
}
default:
throw new NotSupportedException(SR.Get("UnexpectedCollectionChangeAction", new object[] { e.Action }));
}
}
Alternatively, you might hook into the collection NotifyCollectionChanged event from one of your code-behind classes and explicitly change the current selection before the event reaches the TreeView (I'm not sure of this solution though because I am not sure of the order in which event delegates are called - the TreeView might get to process the event before you do - but it might work).
Original answer
In my original answer I guessed that you may be encountering a bug in WPF and gave a generic workaround for this kind of situation, which was to replace item.IsSelected = true; with:
Disptacher.BeginInvoke(DispatcherPriority.Input, new Action(() =>
{
item.IsSelected = true;
}));
I explained that the reason this kind of workaround does the trick 90% of the time is that it delays the selection until almost all current operations have finished processing.
When I actually tried the code you posted in your other question I discovered that it was indeed a bug in WPF but found a more direct and reliable workaround. I'll explain how I diagnosed the problem and then describe the workaround.
Diagnosis
I added a SelectedItemChanged handler with a breakpoint in it, and looked at the stack trace. This made it obvious where the problem lies. Here are selected portions of the stack trace:
...
System.Windows.Controls.TreeView.ChangeSelection
...
System.Windows.Controls.TreeViewItem.OnGotFocus
...
System.Windows.Input.FocusManager.SetFocusedElement
System.Windows.Input.KeyboardNavigation.UpdateFocusedElement
System.Windows.FrameworkElement.OnGotKeyboardFocus
System.Windows.Input.KeyboardFocusChangedEventArgs.InvokeEventHandler
...
System.Windows.Input.InputManager.ProcessStagingArea
System.Windows.Input.InputManager.ProcessInput
System.Windows.Input.KeyboardDevice.ChangeFocus
System.Windows.Input.KeyboardDevice.TryChangeFocus
System.Windows.Input.KeyboardDevice.Focus
System.Windows.Input.KeyboardDevice.ReevaluateFocusCallback
...
As you can see, KeyboardDevice has a ReevaluateFocusCallback private or internal method which changes the focus to the parent of the deleted TreeViewItem. This causes a GotFocus event which causes the parent item to be selected. This all happens in the background after your event handler returns.
Solution
Normally in this case I would tell you to just manually .Focus() the TreeViewItem you are selecting. That is difficult here because in a TreeView there is no easy way to get from an arbitrary data item to the corresponding container (there are separate ItemContainerGenerators at each level).
So I think your best solution is to force the focus to the parent node (just where you don't want it to end up), then set IsSelected in the child's data. That way the input manager will never decide it needs to move the focus on its own: It will find the focus already set to a valid IInputElement.
Here is some code to do that:
if(child != null)
{
SomeObject parent = child.Parent;
// Find the currently focused element in the TreeView's focus scope
DependencyObject focused =
FocusManager.GetFocusedElement(
FocusManager.GetFocusScope(tv)) as DependencyObject;
// Scan up the VisualTree to find the TreeViewItem for the parent
var parentContainer = (
from element in GetVisualAncestorsOfType<FrameworkElement>(focused)
where (element is TreeViewItem && element.DataContext == parent)
|| element is TreeView
select element
).FirstOrDefault();
parent.Children.Remove(child);
if(parent.Children.Count > 0)
{
// Before selecting child, first focus parent's container
if(parentContainer!=null) parentContainer.Focus();
parent.Children[0].IsSelected = true;
}
}
This also requires this helper method:
private IEnumerable<T> GetVisualAncestorsOfType<T>(DependencyObject obj) where T:DependencyObject
{
for(; obj!=null; obj = VisualTreeHelper.GetParent(obj))
if(obj is T)
yield return (T)obj;
}
This should be more reliable than using Dispatcher.BeginInvoke because it will work around this particular problem without making any assumptions about input queue ordering, Dispatcher priorities, and so forth.
This works for me (thanks to investigations provided above)
protected override void OnItemsChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
base.OnItemsChanged(e);
if (e.Action == NotifyCollectionChangedAction.Remove)
{
Focus();
}
}
According to the answer provided by #Kirill I think the correct answer to this specific question would be the following code added to a class derived from TreeView.
protected override void OnItemsChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Remove && SelectedItem != null)
{
var index = Items.IndexOf(SelectedItem);
if (index + 1 < Items.Count)
{
var item = Items.GetItemAt(index + 1) as TreeViewItem;
if (item != null)
{
item.IsSelected = true;
}
}
}
}
Based on the answers above, here's the solution that worked for me (it has fixed various other problems as well, such as the loss of focus after selecting an item via model etc.)
Note the OnSelected override (scroll all the way down) which actually did the trick.
This was compiled in VS2015 for Net 3.5.
using System.Windows;
using System.Windows.Controls;
using System.Collections.Specialized;
namespace WPF
{
public partial class TreeViewEx : TreeView
{
#region Overrides
protected override DependencyObject GetContainerForItemOverride()
{
return new TreeViewItemEx();
}
protected override bool IsItemItsOwnContainerOverride(object item)
{
return item is TreeViewItemEx;
}
#endregion
}
public partial class TreeViewItemEx : TreeViewItem
{
#region Overrides
protected override DependencyObject GetContainerForItemOverride()
{
return new TreeViewItemEx();
}
protected override bool IsItemItsOwnContainerOverride(object item)
{
return item is TreeViewItemEx;
}
protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Remove:
if (HasItems)
{
int newIndex = e.OldStartingIndex;
if (newIndex >= Items.Count)
newIndex = Items.Count - 1;
TreeViewItemEx item = ItemContainerGenerator.ContainerFromIndex(newIndex) as TreeViewItemEx;
item.IsSelected = true;
}
else
base.OnItemsChanged(e);
break;
default:
base.OnItemsChanged(e);
break;
}
}
protected override void OnSelected(RoutedEventArgs e)
{
base.OnSelected(e);
Focus();
}
#endregion
}
}
Related
Same with the subject, When i add some item in ListBox, also Listbox's Scroll Bar is moved automatically.
Because verticalOffset is same, but ExtraHeight is enlarged.
I Just want Don't move scrollBar when i insert some item...
I use both of ObservableCollection's Insert(0, Object) method and Add() method
The Symptom appears when VerticalOffSet is not 0 nor max Height.
You can see that like below
→
And i already moved it manually(with Code)
But when i move it original position, I should watch the move animation.
Did you have an idea?
Plz know me about that.
In WPF the ItemsControl maintains the scroll offset relative to the beginning of the list, forcing items in the viewport to move down when items are added to the ItemsSource. UWP allows to customize this behavior e.g. to behave the way you need it.
I recommend to extend ListBox or alternatively create an attached behavior.
The following example extends ListBox and handles the internal ScrollViewer to adjust the scroll offset to keep the first visible item in the viewport when items are added to the ItemsSource:
class KeepItemsInViewListBox : ListBox
{
private ScrollViewer ScrollViewer { get; set; }
#region Overrides of FrameworkElement
/// <inheritdoc />
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
if (TryFindVisualChildElement(this, out ScrollViewer scrollViewer))
{
this.ScrollViewer = scrollViewer;
}
}
#endregion
#region Overrides of ListView
/// <inheritdoc />
protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
{
base.OnItemsChanged(e);
if (this.ScrollViewer == null)
{
return;
}
double verticalOffset;
switch (e.Action)
{
case NotifyCollectionChangedAction.Add when e.NewItems != null:
// Check if insert or add
verticalOffset = e.NewStartingIndex < this.ScrollViewer.VerticalOffset
? this.ScrollViewer.VerticalOffset + e.NewItems.Count
: this.ScrollViewer.VerticalOffset;
break;
case NotifyCollectionChangedAction.Remove when e.OldItems != null:
verticalOffset = this.ScrollViewer.VerticalOffset - e.OldItems.Count;
break;
default:
verticalOffset = this.ScrollViewer.VerticalOffset;
break;
}
this.ScrollViewer?.ScrollToVerticalOffset(verticalOffset);
}
#endregion
public bool TryFindVisualChildElement<TChild>(DependencyObject parent, out TChild childElement)
where TChild : FrameworkElement
{
childElement = null;
if (parent == null)
{
return false;
}
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
{
DependencyObject child = VisualTreeHelper.GetChild(parent, i);
if (child is TChild resultElement)
{
childElement = resultElement;
return true;
}
if (TryFindVisualChildElement(child, out childElement))
{
return true;
}
}
return false;
}
}
The class can be enhanced by making the behavior optional e.g., by introducing an enum.
I have a search window, which looks like following:
The part with Condition and Options is a ContentControl with several DataTemplates, which contain different filter form for specific field (eg. datetime picker etc.).
I'd like specific control in the DataTemplate to be focused after opening the window (this is the X problem if someone asked)
I'm doing that in the following way:
public FindWindow(FindModel model)
{
InitializeComponent();
this.viewModel = Dependencies.Container.Instance.Resolve<FindWindowViewModel>(new ParameterOverride("access", this), new ParameterOverride("model", model));
DataContext = viewModel;
FocusInput();
}
FocusInput does the following:
public static FrameworkElement GetControlByName(DependencyObject parent, string name)
{
int count = VisualTreeHelper.GetChildrenCount(parent);
for (var i = 0; i < count; ++i)
{
var child = VisualTreeHelper.GetChild(parent, i) as FrameworkElement;
if (child != null)
{
if (child.Name == name)
{
return child;
}
var descendantFromName = GetControlByName(child, name);
if (descendantFromName != null)
{
return descendantFromName;
}
}
}
return null;
}
public void FocusInput()
{
Dispatcher.Invoke(DispatcherPriority.ContextIdle, new Action(() =>
{
var obj = GetControlByName(filterContainer, "input");
if (obj != null && obj is Control ctl)
ctl.Focus();
}));
}
When it runs in the ctor, FindWindow gets null obj (despite ContentControl having Content set). However, when you click "Test" button, which simply runs FocusControl, the latter in turn finds required control and focuses it.
The question is: how to capture moment, when ContentControl finishes instantiating DataTemplate, such that I can capture required control? (Problem Y)
I'll be grateful for solution to either problem X or Y (which is my attempted solution).
Try to call FocusInput() once the window or ContentControl has been loaded:
public FindWindow(FindModel model)
{
InitializeComponent();
this.viewModel = Dependencies.Container.Instance.Resolve<FindWindowViewModel>(new ParameterOverride("access", this), new ParameterOverride("model", model));
DataContext = viewModel;
Loaded += (s, e) => FocusInput();
}
Maybe, a better solution would be behavior. It is inherited from
using System.Windows.Interactivity;
And is very similar to the previous answer, and is reusable
public class FocusBehavior : Behavior<TextBox>
{
protected override void OnAttached()
{
base.OnAttached();
if(AssociatedObject!=null)
{
//LOADED EVENT SUBSCRIBE
AssociatedObject.Loaded += //YOUR FOCUS METHOD;
}
}
protected override void OnDetaching()
{
base.OnDetaching();
//LOADED EVENT UNSUBSCRIBE
AssociatedObject.Loaded -= //YOUR FOCUS METHOD;
}
}
and then attach it to your element in XAML:
<TextBox
x:Name="MainText">
<i:Interaction.Behaviors>
<behaviors:FocusBehavior />
</i:Interaction.Behaviors>
</TextBox>
Source code is here:
https://github.com/djangojazz/BubbleUpExample
The problem is I am wanting an ObservableCollection of a ViewModel to invoke an update when I update a property of an item in that collection. I can update the data that it is bound to just fine, but the ViewModel that holds the collection is not updating nor is the UI seeing it.
public int Amount
{
get { return _amount; }
set
{
_amount = value;
if (FakeRepo.Instance != null)
{
//The repo updates just fine, I need to somehow bubble this up to the
//collection's source that an item changed on it and do the updates there.
FakeRepo.Instance.UpdateTotals();
OnPropertyChanged("Trans");
}
OnPropertyChanged(nameof(Amount));
}
}
I basically need the member to tell the collection where ever it is called: "Hey I updated you, take notice and tell the parent you are a part of. I am just ignorant of bubble up routines or call backs to achieve this and the limited threads I found were slightly different than what I am doing. I know it could possible be done in many ways but I am having no luck.
In essence I just want to see step three in the picture below without having to click on the column first.
Provided that your underlying items adhere to INotifyPropertyChanged, you can use an observable collection that will bubble up the property changed notification such as the following.
public class ItemObservableCollection<T> : ObservableCollection<T> where T : INotifyPropertyChanged
{
public event EventHandler<ItemPropertyChangedEventArgs<T>> ItemPropertyChanged;
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs args)
{
base.OnCollectionChanged(args);
if (args.NewItems != null)
foreach (INotifyPropertyChanged item in args.NewItems)
item.PropertyChanged += item_PropertyChanged;
if (args.OldItems != null)
foreach (INotifyPropertyChanged item in args.OldItems)
item.PropertyChanged -= item_PropertyChanged;
}
private void OnItemPropertyChanged(T sender, PropertyChangedEventArgs args)
{
if (ItemPropertyChanged != null)
ItemPropertyChanged(this, new ItemPropertyChangedEventArgs<T>(sender, args.PropertyName));
}
private void item_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
OnItemPropertyChanged((T)sender, e);
}
}
You should do two things to get it to work:
first: you should refactor the RunningTotal property so it can raise the property changed event. Like so:
private int _runningTotal;
public int RunningTotal
{
get => _runningTotal;
set
{
if (value == _runningTotal)
return;
_runningTotal = value;
OnPropertyChanged(nameof(RunningTotal));
}
}
Second thing you should do is calling the UpdateTotals after you add a DummyTransaction to the Trans. An option could be to refactor the AddToTrans method in the FakeRepo
public void AddToTrans(int id, string desc, int amount)
{
Trans.Add(new DummyTransaction(id, desc, amount));
UpdateTotals();
}
I have to disable the OnPaintBackground on my TableLayoutPanel to remove flickering caused from the background being drawn first(because I am drawing on the TLP with the paint method, and yes I need a TLP because it contains many controls for a purpose). So my code is as follows:
public static bool FlickerPanel = false;
public class FlickerTableLayoutPanel : TableLayoutPanel
{
protected override void OnPaintBackground(PaintEventArgs e)
{
if (FlickerPanel)
base.OnPaintBackground(e);
}
}
Then in my paint method I have it draw it's own background. So during runtime it is fine.
Edit: I discovered the root of the problem. By overriding the OnPaintBackground it disables whatever code is making the designer draw the background. If I remove the override all together it doesn't have the graphical glitch.
protected override void OnPaintBackground(PaintEventArgs e)
{
base.OnPaintBackground(e);
}
Even this above code disabled the Design view rendering and causes graphical glitches. Any help much appreciated!
I also had problems detecting whether my form was in design mode. I solved it as follows:
Firstly, I had to write an IsDesignMode() method:
public static bool IsDesignMode(Control control)
{
if (LicenseManager.UsageMode == LicenseUsageMode.Designtime) // Initial quick check.
{
return true;
}
while (control != null)
{
System.ComponentModel.ISite site = control.Site;
if ((site != null) && (site.DesignMode))
return true;
control = control.Parent;
}
return false;
}
I put that method in a shared library assembly (namespace "Windows.Forms.Utilities" in the example below), since I use it in a lot of projects.
Then for each user or custom control where I need to know if it's in design mode, I have to add a private bool _isDesignMode field and override OnHandleCreated() as follows:
protected override void OnHandleCreated(EventArgs e)
{
base.OnHandleCreated(e);
_isDesignMode = Windows.Forms.Utilities.IsDesignMode(this);
}
Then I can use _isDesignMode wherever I need to.
You could surround the code in the OnPaintBackground method with an if statement to detect if your in design time like this:
if (System.ComponentModel.LicenseManager.UsageMode ==
System.ComponentModel.LicenseUsageMode.Designtime)
I just wanted to contribute my slight upgrade to the solution that #matthew-watson provided. As an extension method, the syntax becomes a bit more pleasing to read.
public static partial class ExtensionMethods
{
public static bool IsInDesignMode(this Control source)
{
var result = LicenseManager.UsageMode == LicenseUsageMode.Designtime;
while (result == false && source != null)
{
result = source.Site != null && source.Site.DesignMode;
source = source.Parent;
}
return result;
}
}
and used like this
protected override void OnPaintBackground(PaintEventArgs pevent)
{
if (this.IsInDesignMode())
{
base.OnPaintBackground(pevent);
}
}
just do nothing onPaintBackground() function. Its prevent backgroundImage to be drawn and should fix the flickering.
protected override void OnPaintBackground(PaintEventArgs e)
{
}
There is a way to get who lost his focus in a c# form without using the LostFocus event each component?
[edit]
I need for a On Screen Keyboard.
I need to store last focussed control to fire keypress, but i need to do it to all in the window.
Also the main project is wpf, than i have some component nested as itemsTemplate and so on...
I finally used this:
foreach (Control uie in FindInLogicalTreeDown(this, typeof(TextBox))) AssignEvents(uie);
private static IEnumerable<DependencyObject> FindInLogicalTreeDown(DependencyObject obj, Type type)
{
if (obj != null)
{
if (obj.GetType() == type) { yield return obj; }
foreach (object child in LogicalTreeHelper.GetChildren(obj))
if (typeof(DependencyObject).IsAssignableFrom(child.GetType()))
foreach (var nobj in FindInLogicalTreeDown((DependencyObject)child, type)) yield return nobj;
}
yield break;
}
void AssignEvents(Control element)
{
element.GotMouseCapture += new MouseEventHandler(Component_GotFocus);
}
public Control LastFocus { get; set; }
public void Component_GotFocus(object sender, RoutedEventArgs e)
{
LastFocus = (Control)sender;
if (LastFocus.GetType() == typeof(TextBox)) { KeyboardVisible = true; }
}
i don't think there is any way until unless you subscribe events and keep track which lost focus event has fired last