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.

Simplify app deployment in .NET

11. January 2013 01:51 by Mrojas in   //  Tags: , , , , , , , , , ,   //   Comments (0)

 

The web platform provides an excellent mechanism simplify your app distribution and deployment issues.  

There is no longer a need for CD-ROMs, or to send a computer technician to install the app on each client computer. Other related problems such as verifying that the client has the right application version, application dependencies and security can be simplified.  

When you modernize your legacy app with Mobilize.Net\Artinsoft you could take advantage of several options. In terms of application distribution\deployment thru web technologies or running your application inside a web browser we have several post describing different way of doing this.  
 

Using ClickOnce deployment over the web to simplify Windows Form Application deployment 

Embedding Windows Forms Applications directly in a WebBrowser  

Embedding Windows Forms Applications in a WebBrowser using XBAP  

Silverlight as a mechanism for simplification of application deployment  

WPF applications distribution on the browser using XBAP  

 

Use HTML5 deploy your applications everywhere 

WPF applications deployment on the browser using XBAP

11. January 2013 01:44 by Mrojas in   //  Tags: , , , ,   //   Comments (0)

WPF is great and powerful technology to create compelling Windows Desktop applications when you need a rich user interface. 

Artinsoft/Mobilize.NET provide tools and services that allows you to modernize your VB6, Windows Forms and Powebuilder apps to WPF. Once your applications are on WPF you can also benefit from features like XBAP... 

From Wikipedia: 

"XAML Browser Applications (XBAP, pronounced "ex-bap") are Windows Presentation Foundation (.xbap) applications that are hosted and run inside a web browser such asFirefox or Internet Explorer. Hosted applications run in a partial trust sandbox environment and are not given full access to the computer's resources like opening a new network connection or saving a file to the computer disk and not all WPF functionality is available. The hosted environment is intended to protect the computer from malicious applications; however it can also run in full trust mode by the client changing the permission." 

XBAP is in general a simplification of the Click Once deployment, but specifically for WPF applications. 

Debug XBAP using WinForms Host

26. January 2009 06:13 by Mrojas in General  //  Tags: , , , , ,   //   Comments (0)

Recently I had to deal with targeting an XBAP application that had some Windows Forms controls.

The problem is that those controls can only be used in a trusted environment. If you try to debug an XBAP with some Windows Forms Controls you will get an exception like:

Message: Cannot create instance of 'Page1' defined in assembly 'XBAPWithWinForms, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'. Exception has been thrown by the target of an invocation.  Error in markup file 'Page1.xaml' Line 1 Position 7.

It took me a while to found a solution, and it was thru Scott Landford Blog that I found a way around.

In short what he recommends to do is:

Change your settings to:

Start Action->Start external program = %windir%system32\PresentationHost.exe

  In my case (and the case of most people that is: c:\windows\system32\PresentationHost.exe)


Start Options->Command line arguments = -debug "c:\projects\myproject\bin\debug\MyProject.xbap" -debugSecurityZoneUrl "http://localhost:2022"

Copy the value from the Start URL after the –debug argument

Very import for using WinForms components you must run in FULL TRUST

fullTrust

 

Here is some XBAP code using a WinForms WebBrowser. They designer does not like it a lot but it works:

XBAPWithWinforms.zip

Categories