Binding Events in WPF Templates

6. April 2013 14:14 by Mrojas in   //  Tags: , , , ,   //   Comments (0)

Recently we come up with a situation during a migration
to WPF where we needed to create a control that used an
item template and we needed to bind the item template
control to a control.

WPF in general is a framework that has great support for MVVM.
The DataBinding is great, but what about binding events.
Well... I don't know why WPF does not have an out-of-the box support
for event binding.
The MVVM way to do this binding is to use Commanding (see ...), but
is not that natural.

In this post I will describe how we worked out a solution for binding
an event, in particular Click Event for controls defined in an ItemTemplate.

So, all this story started when we had an itemtemplate like:

<HierarchicalDataTemplate x:Key="
  CheckBoxTreeViewItemTemplate" 
  ItemsSource="{Binding Items, Mode=OneWay}" >
 <StackPanel Orientation="Horizontal">
  <Image Margin="2,0" Source="{Binding ImageSource, Mode=TwoWay}" />
     <CheckBox Focusable="False" IsChecked="{Binding IsChecked}" VerticalAlignment="Center"/>
     <ContentPresenter Content="{Binding Text, Mode=TwoWay}" Margin="2,0" />
 </StackPanel>
</HierarchicalDataTemplate>


The idea was to create a collection of CheckBoxTreeViewItem(s) and bind that
collection to a control like a TreeView.
We also had a restriction. We did not wanted to subclass the TreeView
or any other control.

Binding an Event to an ItemTemplate

Google took me to this post:

http://stackoverflow.com/questions/2974981/wpf-datatemplate-event-binding-to-object-function

This gave me a great guide on how to bind the event item.

The starting code for our item was:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Windows.Controls;

    public class CheckedTreeViewItem : ItemsControl, INotifyPropertyChanged
    {
        bool? _isChecked = false;
        string _imageSource = String.Empty;
        int _imageIndex = -1;

        public TreeView TreeView
        {
            get
            {
                if (this.Parent != null && this.Parent is TreeView)
                {
                    return this.Parent as TreeView;
                }
                else if (this.Parent != null && this.Parent is CheckedTreeViewItem)
                {
                    return ((CheckedTreeViewItem)this.Parent).TreeView;
                }
                else
                {
                    return null;
                }
            }
        }

        public string ImageSource
        {
            get { return _imageSource; }
            private set { _imageSource = value;}
        }

        public bool? IsChecked
        {
            get { return _isChecked; }
            set { this.SetIsChecked(value, true, true); }
        }

        public bool IsExpanded
        {
            get;
            set;
        }

        public string Text { get; set; }

        public string Key { get; set; }

        public int ImageIndex 
        {
            get
            {
                return _imageIndex;
            }
            set 
            {
                _imageIndex = value;
                if (_imageIndex >= 0)
                {
                    var imageList = UpgradeHelpers.VB6.WPF.ImageList.ImageListAttachedProperties.GetImageList(TreeView);
                    var extractedSource = ((Image)imageList.Items[_imageIndex]).Source;
     _imageSource = extractedSource.ToString();
                }
                else
                {
                    _imageSource = String.Empty;
                }
                
            }
        }

        public bool IsInitiallySelected { get; private set; }

        public event PropertyChangedEventHandler PropertyChanged;

        void SetIsChecked(bool? value, bool updateChildren, bool updateParent)
        {
            if (value == _isChecked)
                return;

            _isChecked = value;

            if (updateChildren && _isChecked.HasValue)
            {
                foreach (CheckedTreeViewItem node in this.Items)
                {
                    node.SetIsChecked(_isChecked, true, false);
                }
            }

            if (updateParent && this.Parent != null && this.Parent is CheckedTreeViewItem)
                ((CheckedTreeViewItem)this.Parent).VerifyCheckState();

            this.OnPropertyChanged("IsChecked");
        }

        protected void OnPropertyChanged(string property)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(property));
            }
        }

        void VerifyCheckState()
        {
            bool? state = null;
            for (int i = 0; i < this.Items.Count; ++i)
            {
                bool? current = ((CheckedTreeViewItem)this.Items[i]).IsChecked;
                if (i == 0)
                {
                    state = current;
                }
                else if (state != current)
                {
                    state = null;
                    break;
                }
            }
            this.SetIsChecked(state, false, true);
        }

    }


I needed a property exposing a command, so we could bind a controls
Command to a ClickEventHandler. The event should be defined as an
attached event. Why? Because we wanted users of this item to be able
to define for example in the TreeView an event handler that will be
then assigned to all my checkboxes. So if a users clicks one of the
checkboxes this event will be called.

// This event uses the bubbling routing strategy
              public static readonly RoutedEvent CheckedEvent = EventManager.RegisterRoutedEvent(
                     "Checked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(TreeView));
 
              public static void AddCheckedHandler(DependencyObject d, RoutedEventHandler handler)
              {
                     UIElement uie = d as UIElement;
                     if (uie != null)
                     {
                           uie.AddHandler(CheckedTreeViewItem.CheckedEvent, handler);
                     }
              }
              public static void RemoveCheckedHandler(DependencyObject d, RoutedEventHandler handler)
              {
                     UIElement uie = d as UIElement;
                     if (uie != null)
                     {
                           uie.RemoveHandler(CheckedTreeViewItem.CheckedEvent, handler);
                     }
              }
 }

From my item I could obtain a reference to container control, but how
 could I retrieve the RoutedEvent from my container control...

Getting RoutedEvents from a control

<I used this reference http://stackoverflow.com/questions/982709/removing-routed-event-handlers-through-reflection/15854140#15854140>
I was surprised of how difficult and tricky this was. But this is how I did it:

// Get the control's Type
  Type controlViewType = ((UIElement)control).GetType();
 
  // Dig out the undocumented (yes, I know, it's risky) EventHandlerStore
  // from the control's Type
  PropertyInfo EventHandlersStoreType =
  controlViewType.GetProperty("EventHandlersStore",
  BindingFlags.Instance | BindingFlags.NonPublic);
 
  // Get the actual "value" of the store, not just the reflected PropertyInfo
  Object EventHandlersStore = EventHandlersStoreType.GetValue(tree, null);
  var miGetRoutedEventHandlers = EventHandlersStore.GetType().GetMethod("GetRoutedEventHandlers", BindingFlags.Public | BindingFlags.Instance);
  RoutedEventHandlerInfo[] res = (RoutedEventHandlerInfo[])miGetRoutedEventHandlers.Invoke(EventHandlersStore, new object[] { CheckedTreeViewItem.CheckedEvent });

  
  After doing that I could get the methodinfo an invoke that code thru reflection.
  So invoking the code is tricky too. When controls are put on the designer,
  the event handler are usually added to the window or page. So to retrieve the target I
  need to do the following:
  
  

var parent = VisualTreeHelper.GetParent(control);
  while (!(control is Window) && !(control is Page))
  {
    parent = VisualTreeHelper.GetParent(parent);
  }

  With that I can create an action to call the event handler like this:
  
      _handler = () => {
                           res.First().Handler.Method.Invoke(parent, new object[] { control, new RoutedEventArgs() })

 
  
 
Now back to binding an event to a template item thu a Command 

I created a new class called like the one on the post:

public class BindToClickEventCommand : ICommand
    {
              private Action _handler;
 
              public ViewModelCommand(Control control)
              {
                    
                     // Get the control's Type
                     Type someTreeViewType = ((UIElement)control).GetType();
 
                     // Dig out the undocumented (yes, I know, it's risky) EventHandlerStore
                     // from the control's Type
                     PropertyInfo EventHandlersStoreType =
                                  someTreeViewType.GetProperty("EventHandlersStore",
                                  BindingFlags.Instance | BindingFlags.NonPublic);
 
                     // Get the actual "value" of the store, not just the reflected PropertyInfo
                     Object EventHandlersStore = EventHandlersStoreType.GetValue(control, null);
                     var mi= EventHandlersStore.GetType().GetMethod("GetRoutedEventHandlers", BindingFlags.Public | BindingFlags.Instance);
                     RoutedEventHandlerInfo[] res = (RoutedEventHandlerInfo[])mi.Invoke(EventHandlersStore, new object[] { CheckedTreeViewItem.CheckedEvent });
                     var parent = VisualTreeHelper.GetParent(control);
                     while (!(parent is Window))
                     {
                           parent = VisualTreeHelper.GetParent(parent);
                     }
 
 
                     _handler = () => {
                           res.First().Handler.Method.Invoke(parent, new object[] { control, new RoutedEventArgs() });
                     };
                    
              }
 
              #region ICommand Members
 
              public bool CanExecute(object parameter)
              {
                     return true;
              }
 
              public event EventHandler CanExecuteChanged;
 
              public void Execute(object parameter)
              {
                     _handler();
              }
 
              #endregion
       }


 

And I added a property to the CheckedTreeViewItem

public BindToClickEventCommand CheckedEvent
{
 get
 {
  return new BindToClickEventCommand(TreeView);
 }
}


And bind that property by changing my template to:

<HierarchicalDataTemplate x:Key="CheckBoxTreeViewItemTemplate" ItemsSource="{Binding Items, Mode=OneWay}" >
        <StackPanel Orientation="Horizontal">
            <Image Margin="2,0" Source="{Binding ImageSource, Mode=TwoWay}" />
                     <CheckBox Focusable="False" IsChecked="{Binding IsChecked}" VerticalAlignment="Center"
                                    Command="{Binding CheckedEvent}"/>
                     <ContentPresenter Content="{Binding Text, Mode=TwoWay}" Margin="2,0" />
        </StackPanel>
    </HierarchicalDataTemplate>


 
 
In the container control all that is needed is to add the attached event like

<TreeView ...    local:CheckBoxTreeViewItem.Checked="item_Checked"



 
Once we did that we achieved our purpose. It is not perfect,
but due to some of the restrictions this is how we achieved it.

Categories