Removing event handlers from custom control's template parts - wpf

When I first started writing WPF custom controls, if I wanted to add an event handler, I would do so in the control's OnApplyTemplate override, after getting the template part:
public void override OnApplyTemplate() {
if ( addMenu != null ) {
addMenu.Click -= addMenu_Click;
addMenu = null;
}
addMenu = (MenuItem)Template.FindName("PART_AddMenu", this);
addMenu.Click += addMenu_Click;
}
But then one day I noticed that OnApplyTemplate() is not always called when I'd expect it to be, i.e. when the control is disconnected from the visual tree. That is, using the above technique, the event handlers won't always be removed. So I came up with a different way:
public MyCustomControl()
{
Loaded += this_Loaded;
}
void this_Loaded(object sender, RoutedEventArgs e)
{
Unloaded += this_Unloaded;
addMenu = (MenuItem)Template.FindName("PART_AddMenu", this);
addMenu.Click += addMenu_Click;
}
void this_Unloaded(object sender, RoutedEventArgs e)
{
Unloaded -= this_Unloaded;
if (addMenu != null)
{
addMenu.Click -= addMenu_Click;
addMenu = null;
}
}
This way seems to do the trick. Does everyone concur that this is the better way of hooking up and removing event handlers in a custom control? If not, then why?

This method is fine, but you do have to understand that you get the unloaded event at times that you might not want the event handlers unhooked. For example, let's say you have a tab control. When you switch TabItems the content of the previous TabItem all gets Unloaded and then reloaded when the TabItem becomes selected again. This is fine for things like Button.Click because you can't perform such actions on an inactive tab, but any events that don't require the item to be loaded into the visual tree will be disconnected even though the items still exist.
Why do you feel you need to clean up all event handlers? I realize that there are some cases where they can hang onto a reference of another object, but this is an unusual case and is usually best handled by cleaning them up when used in that way. Here's some better details on this: How built-in WPF controls manage their event handlers to an attached event?

WPF controls (such as ComboBox) uses the OnTemplateChangedInternal() method to unregister events that are registered in OnApplyTemplate(). You can't override that method, as it's internal to the PresentationFramework dll, but you can override the protected OnTemplateChanged() method to do the same - it's called by OnTemplateChangedInternal() in the Control base class.
Here's sample code that could go into your custom control:
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
EditableTextBoxSite = GetTemplateChild("PART_EditableTextBox") as TextBox;
EditableTextBoxSite.TextChanged += new TextChangedEventHandler(this.OnEditableTextBoxTextChanged);
this.EditableTextBoxSite.PreviewTextInput -= new TextCompositionEventHandler(this.OnEditableTextBoxPreviewTextInput);
}
protected override void OnTemplateChanged(ControlTemplate oldTemplate, ControlTemplate newTemplate)
{
base.OnTemplateChanged(oldTemplate, newTemplate);
if (this.EditableTextBoxSite == null)
return;
this.EditableTextBoxSite.TextChanged -= new TextChangedEventHandler(this.OnEditableTextBoxTextChanged);
this.EditableTextBoxSite.PreviewTextInput -= new TextCompositionEventHandler(this.OnEditableTextBoxPreviewTextInput);
}
I'm not sure about all the implications of doing this, but it does seem to be the closest way to emulate what WPF controls do.

Related

Where to unsubcribe from an event for a custom control

I am wondering at what point do I unsubscribe from an event, up to this point I was usually unsubscribing on the line before subscribing (or most of the time the event was called from xaml, which then is handled by xaml and there is no need to do any extra work).
But now I'm in a situation when I want to subscribe at the constructor so, where do I unsubscribe? I tried to do it inside unloaded event,
but my control is often unloaded and then loaded again without recreating it.
Edit
To make it clear I want to unsubscribe when the object is not needed anymore, I was hoping that there is Dispose method I can override or something like this.
Any ideas?
Sample code
public class MYListBox : ListBox
{
public MYListBox()
{
SelectionChanged += MYListBox_SelectionChanged;
Unloaded += MYListBox_Unloaded;
}
private void MYListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
}
private void MYListBox_Unloaded(object sender, RoutedEventArgs e)
{
SelectionChanged -= MYListBox_SelectionChanged;
Unloaded -= MYListBox_Unloaded;
}
}
xaml
<UserControl x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:wpfApplication1="clr-namespace:WpfApplication1">
<wpfApplication1:MYListBox />
</UserControl >
So sometimes, I will allow the user navigate somewhere else and then allow it to comeback to the same instance. Another time when the user navigates when he comes back it will be a new instance. So at the second situation I thought I need to unsubcribe from that event.
Thank you :)
It looks like you've used ListBox as a base class, is there a reason you can't just override the SelectionChanged event and not deal with the event subscriptions?
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
// Do custom work.
// Call base class implementation.
base.OnSelectionChanged(e);
}

Controlling the priority of children's handlers for the same keyboard event

I have multiple children of the same WinForms form, each with its own handler for a keyboard event. For a minimal example (C#):
public Form1() {
InitializeComponent();
c1 = new Control();
c2 = new Control();
c1.KeyPress += c1_KeyPress;
c2.KeyPress += c2_KeyPress;
Controls.Add(c1);
Controls.Add(c2);
}
void c1_KeyPress(object sender, KeyPressEventArgs e) {
Text += " c1";
e.Handled = true;
}
void c2_KeyPress(object sender, KeyPressEventArgs e) {
Text += " c2";
e.Handled = true;
}
When the event fires, it always gets handled by whichever child was originally added to the form first. Reordering the children with c2.BringToFront() or Controls.SetChildIndex(c2, 0) doesn't change the priority. Reordering the constructions or the delegate assignments doesn't change anything either. Calling c2.Focus() doesn't either. Changing the order of the Add calls is the only thing that seems to affect it.
(By contrast, for mouse events the priority gets resolved in an expected way: the topmost control under the pointer hotspot gets dibs on the event, and "topmost" is a clear concept I can control using BringToFront and friends.)
In my real case, c1 is a simple custom control derived from WinForms.UserControl, and c2 is a CefSharp.WinForms.ChromiumWebBrowser. There the keyboard events are caught by c2 no matter what I do.
What decides this priority of handlers? How can I change it?
There is no "priority", keyboard events are raised on the control that has the focus. Intuitively simple to understand, entering text in a TextBox requires selecting it first. A very significant flaw in the posted snippet is that you cannot tell which one has the focus. Although the Control class is usable as-is, in practice you almost always need to derive your own class from it to give it desirable behavior.
Add a new class to your project and paste the code shown below. Replace new Control() with new MyControl(). Now you can tell.
using System;
using System.Windows.Forms;
class MyControl : Control {
protected override void OnEnter(EventArgs e) {
this.Invalidate();
base.OnEnter(e);
}
protected override void OnLeave(EventArgs e) {
this.Invalidate();
base.OnLeave(e);
}
protected override void OnPaint(PaintEventArgs e) {
if (this.Focused) {
ControlPaint.DrawFocusRectangle(e.Graphics, this.DisplayRectangle);
}
base.OnPaint(e);
}
}

Issue with Swipe inside ListView

I am using a ListView in a Windows Store App.
Whenever I start swiping(using simulator tap mode) over the list view all the items move together as illustrated in the picture.
How can I disable this manipulation event?
To your ListView, add:
ScrollViewer.VerticalScrollBarVisibility="Disabled" ScrollViewer.VerticalScrollMode="Disabled"
If that is not enough (this sometimes does not work with MouseWheel events, in that the events still tend to be caught in the ListView and also tends to happen if the list inside of the ScrollViewer is particularly large, I've found), then you need to create a custom control to specifically ignore the event, such as this for PointerWheelChanged.
public class CustomListView : ListView
{
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
var sv = this.GetTemplateChild("ScrollViewer") as UIElement;
if (sv != null)
sv.AddHandler(UIElement.PointerWheelChangedEvent, new PointerEventHandler(OnPointerWheelChanged), true);
}
private void OnPointerWheelChanged(object sender, PointerRoutedEventArgs e)
{
e.Handled = false;
}
}
This will disable mouse wheel scrolling inside of your ListView. You'll have to change your XAML reference to the ListView from <ListView> to <namespace:ListView> where namespace is the namespace you've created your ListView in.

How to set scroll position from view model with caliburn.micro?

I have a ListBox in my view, bound to a collection that is dynamically growing. I would like the scroll position to follow the last added item (which is appended to the bottom of the list). How can I achieve this with Caliburn.Micro?
An alternative could be to use the event aggregator to publish a message to the view.
Something like:
Aggregator.Publish(ItemAddedMessage<SomeItemType>(itemThatWasJustAdded));
and in the view:
public class SomeView : IHandle<ItemAddedMessage<SomeItemType>>
{
public void Handle(ItemAddedMessage<SomeItemType> message)
{
// Implement view specific behaviour here
}
}
It depends on what your requirements are but at least then the view is responsible for display concerns and you can still test the VM
Also you could just implement the code solely in the view - since it appears to be a view concern (e.g. using the events that listbox provides)
A behaviour would also be useful but maybe one that's a little less coupled to your types - e.g. a generic behaviour SeekAddedItemBehaviour which hooks listbox events to find the last item. Not sure if the listbox exposes the required events, but worth a look
EDIT:
Ok this may work full stop - you should be able to just attach this behaviour to the listbox and it should take care of the rest:
public class ListBoxSeekLastItemBehaviour : System.Windows.Interactivity.Behavior<ListBox>
{
private static readonly DependencyProperty ItemsSourceWatcherProperty = DependencyProperty.Register("ItemsSourceWatcher", typeof(object), typeof(ListBoxSeekLastItemBehaviour), new PropertyMetadata(null, OnItemsSourceWatcherPropertyChanged));
private ListBox _listBox = null;
private static void OnItemsSourceWatcherPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ListBoxSeekLastItemBehaviour source = d as ListBoxSeekLastItemBehaviour;
if (source != null)
source.OnItemsSourceWatcherPropertyChanged();
}
private void OnItemsSourceWatcherPropertyChanged()
{
// The itemssource has changed, check if it raises collection changed notifications
if (_listBox.ItemsSource is INotifyCollectionChanged)
{
// if it does, hook the CollectionChanged event so we can respond to items being added
(_listBox.ItemsSource as INotifyCollectionChanged).CollectionChanged += new NotifyCollectionChangedEventHandler(ListBoxSeekLastItemBehaviour_CollectionChanged);
}
}
void ListBoxSeekLastItemBehaviour_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems.Count > 0)
{
// If an item was added seek it
ScrollIntoView(e.NewItems[0]);
}
}
protected override void OnAttached()
{
base.OnAttached();
// We've been attached - get the associated listbox
var box = this.AssociatedObject as ListBox;
if (box != null)
{
// Hold a ref
_listBox = box;
// Set a binding to watch for property changes
System.Windows.Data.Binding binding = new System.Windows.Data.Binding("ItemsSource") { Source = _listBox; }
// EDIT: Potential bugfix - you probably want to check the itemssource here just
// in case the behaviour is applied after the original ItemsSource binding has been evaluated - otherwise you might miss the change
OnItemsSourceWatcherPropertyChanged();
}
}
private void ScrollIntoView(object target)
{
// Set selected item and try and scroll it into view
_listBox.SelectedItem = target;
_listBox.ScrollIntoView(target);
}
}
You probably want to tidy it up a bit and also make sure that the event handler for CollectionChanged is removed when the ItemsSource changes.
Also you might want to call it SeekLastAddedItemBehaviour or SeekLastAddedItemBehavior - I tend to keep the US spelling since it matches Microsoft's spelling. I think SeekLastItem sounds like it will scroll to the last item in the list rather than the last added item
You could reference the view in the view model using GetView(). That also couples the view and view model.
var myView = GetView() as MyView;
myView.MyListBox.DoStuff
Another option is to create a behavior. This is an example of how to use a behavior to expand a TreeView from the view model. The same could be applied to a ListBox.
Actually, there is an easier way to achieve this, without any of the above.
Just extend your Listbox with the following:
namespace Extensions.Examples {
public class ScrollingListBox : ListBox
{
protected override void OnItemsChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
{
int newItemCount = e.NewItems.Count;
if (newItemCount > 0)
this.ScrollIntoView(e.NewItems[newItemCount - 1]);
base.OnItemsChanged(e);
}
}
}
}
Then in Xaml, Declare the Location of your extension class as so:
xmlns:Extensions="clr-namespace:Extensions.Examples"
And when you create your listbox, instead of using
<Listbox></Listbox>
Just use your extended class
<Extensions:ScrollingListBox></Extensions:ScrollingListBox>

Why is the TreeViewItem's MouseDoubleClick event being raised multiple times per double click?

XAML
<TreeView Name="GroupView" ItemsSource="{Binding Documents}">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<EventSetter Event="MouseDoubleClick" Handler="OnTreeNodeDoubleClick"/>
</Style>
</TreeView.ItemContainerStyle>
....
</TreeView>
Code-Behind
private void OnTreeNodeDoubleClick(object sender, MouseButtonEventArgs mouseEvtArgs)
{
Console.WriteLine("{3} MouseDoubleClick Clicks={0} ChangedButton={1} Source={2} Handled={4} ButtonState={5}",
mouseEvtArgs.ClickCount, mouseEvtArgs.ChangedButton, mouseEvtArgs.OriginalSource,
mouseEvtArgs.Timestamp, mouseEvtArgs.Handled, mouseEvtArgs.ButtonState);
}
I find that for one double click, the event handler is called multiple times. I'm trying to open up a document in tab on a double-click on the corresponding tree node; so I'd need to filter out the extra calls.
23479156 MouseDoubleClick Clicks=1 ChangedButton=Left Source=System.Windows.Controls.TextBlock Handled=False ButtonState=Pressed
23479156 MouseDoubleClick Clicks=1 ChangedButton=Left Source=System.Windows.Controls.TextBlock Handled=False ButtonState=Pressed
In my slightly-complicated app, it is being raised 4 times per double-click. On a simple repro-app, it is being raised 2 times per double click. Also all the event argument parameters are the same too, so I can't distinguish the last one of a set.
Any ideas why this is the way it is?
I know this is an old question, but as I came across it in my searches for the solution, here are my findings for any future visitors!
TreeViewItems are recursively contained within each other. TreeViewItem is a HeaderedContentControl (see msdn), with the child nodes as the Content. So, each TreeViewItem's bounds include all of its child items. This can be verified using the excellent WPF Inspector by selecting a TreeViewItem in the visual tree, which will highlights the bounds of the TreeViewItem.
In the OP's example, the MouseDoubleClick event is registered on each TreeViewItem using the style. Therefore, the event will be raised for the TreeViewItems that you double-clicked on - and each of its parent items - separately. This can be verified in your debugger by putting a breakpoint in your double-click event handler and putting a watch on the event args' Source property - you will notice that it changes each time the event handler is called. Incidentally, as can be expected, the OriginalSource of the event stays the same.
To counter this unexpected behaviour, checking whether the source TreeViewItem is selected, as suggested by Pablo in his answer, has worked the best for me.
When a TreeViewItem is double clicked, that item is selected as part of the control behavior. Depending on the particular scenario it could be possible to say:
...
TreeViewItem tviSender = sender as TreeViewItem;
if (tviSender.IsSelected)
DoAction();
...
I've done some debugging and it appears to be a bug in WPF. Most answers already given are correct, and the workaround is to check if the tree view item is selected.
#ristogod's answer is the closest to the root problem - it mentions that setting e.Handled = true the first time handler is invoked doesn't have the desired effect and the event continues to bubble up, calling parent TreeViewItems' handlers (where e.Handled is false again).
The bug seems to be in this code in WPF:
http://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/Windows/Controls/Control.cs,5ed30e0aec6a58b2
It receives the MouseLeftButtonDown event (which is handled by the child control already), but it fails to check if e.Handled is already set to true. Then it proceeds to create a new MouseDoubleClick event args (with e.Handled == false) and invokes that always.
The question also remains why after having set it to handled the first time the event continues to bubble? Because in this line, when we register the handler Control.HandleDoubleClick:
http://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/Windows/Controls/Control.cs,40
we pass true as the last argument to RegisterClassHandler:
http://referencesource.microsoft.com/#PresentationCore/Core/CSharp/System/Windows/EventManager.cs,161
which is handledEventsToo.
So the unfortunate behavior is a confluence of two factors:
Control.HandleDoubleClick is called always (for handled events too), and
Control.HandleDoubleClick fails to check if the event had already been handled
I will notify the WPF team but I'm not sure this bug is worth fixing because it might break existing apps (who rely on the current behavior of event handlers being called even if Handled was set to true by a previous handler).
private void TreeView_OnItemMouseDoubleClick(object sender, MouseButtonEventArgs e)
{
if (e.Source is TreeViewItem
&& (e.Source as TreeViewItem).IsSelected)
{
// your code
e.Handled = true;
}
}
This is not actually a bubbling issue. I've seen this before. Even when you tell the event that you handled it, it continues to keep bubbling up. Except that I don't think that it's actually bubbling up, but rather firing the node above's own double click event. I could be totally wrong on that. But in either case, it's important to know that saying:
e.handled = true;
Does nothing to stop this from happening.
One way to prevent this behavior is to note that when you are double clicking, you are first single clicking and that the selected event should fire first. So while you can't stop the Double Click events from occurring, you should be able to check inside the handler to see whether the event logic should run. This example leverages that:
TreeViewItem selectedNode;
private void MouseDoubleClickEventHandler(object sender, MouseButtonEventArgs e)
{
if(selectedNode = e.Source)
{
//do event logic
}
}
private void TreeViewSelectedEventHandler(object sender, RoutedEventArgs e)
{
selectedNode = (TreeViewItem)e.Source;
}
Sometimes however you have situations where the nodes are being selected by other beans than through the TreeView SelectedItemChanged event. In that case you can do something like this. If you happen to have a TreeView with a single declared top node, you can give that node a specific name and then do something like this:
bool TreeViewItemDoubleClickhandled;
private void MouseDoubleClickEventHandler(object sender, MouseButtonEventArgs e)
{
if (!TreeViewItemDoubleClickhandled)
{
//do logic here
TreeViewItemDoubleClickhandled = true;
}
if (e.Source == tviLoadTreeTop)
{
TreeViewItemDoubleClickhandled = false;
}
e.Handled = true;
}
Regardless of the method you use, the important thing is to note that for whatever reason with TreeViewItem double clicking that you can't stop the events from firing up the tree. At least I haven't found a way.
I have a little bit more elegant solution than checking for selection or creating flags:
A helper method:
public static object GetParent(this DependencyObject obj, Type expectedType) {
var parent = VisualTreeHelper.GetParent(obj);
while (parent != null && parent.GetType() != expectedType)
parent = VisualTreeHelper.GetParent(parent);
return parent;
}
And then your handler:
public void HandleDoubleClick(object sender, MouseButtonEventArgs e)
{
if (e.OriginalSource is DependencyObject)
if (sender == (e.OriginalSource as DependencyObject).GetParent(typeof(TreeViewItem)))
{
// sender is the node, which was directly doubleclicked
}
}
This is the wonderful world of event bubbling. The event is bubbling up the node hierarchy of your TreeView and your handler is called once for every node in the hierarchy path.
Just use something like
// ...
if (sender != this)
{
return;
}
// Your handler code goes here ...
args.Handled = true;
// ...
in your handler code.
There are some pretty major problems with this solution, but it could work in case someone needs to solve this problem in multiple places and I did find a scenario where the accepted solution doesn't work (double clicking on a toggle button that opens up a popup, where the toggle button is inside another element that handles double click.)
public class DoubleClickEventHandlingTool
{
private const string DoubleClickEventHandled = "DoubleClickEventHandled";
public static void HandleDoubleClickEvent()
{
Application.Current.Properties[DoubleClickEventHandled] = DateTime.Now.AddSeconds(1);
}
public static bool IsDoubleClickEventHandled()
{
var doubleClickWasHandled = Application.Current.Properties[DoubleClickEventHandled] as DateTime?;
return doubleClickWasHandled.HasValue && !IsDateTimeExpired(doubleClickWasHandled.Value);
}
private static bool IsDateTimeExpired(DateTime value)
{
return value < DateTime.Now;
}
public static void EnableDoubleClickHandling()
{
Application.Current.Properties[DoubleClickEventHandled] = null;
}
public static bool IsDoubleClickEventHandledAndEnableHandling()
{
var handled = IsDoubleClickEventHandled();
EnableDoubleClickHandling();
return handled;
}
}
Use DoubleClickEventHandlingTool.HandleDoubleClickEvent()
inside the inner/low level element eg:
private void OnPreviewMouseDown(object sender, MouseButtonEventArgs e)
{if (e.ClickCount == 2) DoubleClickEventHandlingTool.HandleDoubleClickEvent();}
High level double click event then only performs it's action when:
if (!DoubleClickEventHandlingTool.IsDoubleClickEventHandledAndEnableHandling())
The most likely reason is that the doubleclick handler is installed multiple times, so each instance of the handler is being called once for each click.

Resources