Posts Tagged ‘Windows Presentation Foundation’

I wrote yesterday about the WPF Snap-To Behavor that I created, and showed you how simple it is, but I skipped over where the magic happens, so I thought I’d go through how I created the Native Behaviors collection class, because there are a few cool tricks in here that I wanted to share with WPF developers at large (even if you’re not interested in hooking the Window Procedure).

The framework for Native Bahaviors is made up of just two classes, the first of which is basically 3 lines of code.

NativeBehavior is the abstract base class for behaviors, and consists of just definitions for the mandatory abstract GetHandlers() method (where you return an IEnumerable collection of mappings from Window Messages to your delegate methods) and the two optional (virtual) methods AddTo and RemoveFrom which are called when your behavior is initially added to a window (generally before the Window is initialized).

NativeBehaviors is an ObservableCollection of NativeBehaviors, obviously ;) . But it’s ever so much more than that. First of all, it has the NativeBehaviors attached property, but it also has the code for actually hooking a WPF window to get the Window Messages, and a WndProc which processes each message against the mappings retrieved from the various NativeBehaviors that have been registered.

Lets start with the attached dependency property. There’s a very clever (and problematic) trick in the way this property is exposed. If you look at this code, you’ll see I create a private NativeBehavior dependency property, and then create public Get/Set accessors for “Behavior” which just call the NativeBehavior property. The reason for this is that if the dependency property is public, or if the public accessors even have the same name … the XAML parser finds the dependency property and uses it directly (bypassing the get/set accesors), which means you have to have an extra element in your XAML markup to initialize the NativeBehaviors collection, or you get something like this screenshot.

The behavior collection doesn't get created...

So instead, we hide the dependency property, and supply a getter which is backed by the dependency property. I should not that although this works great in the .Net Framework, it’s not recognized properly by all of the tools (including Resharper 4.1 and even Expression Blend), so you may have to fiddle with things.


      private static readonly DependencyProperty NativeBehaviorsProperty =
          DependencyProperty.RegisterAttached(
          "NativeBehaviors", typeof(NativeBehaviors), typeof(NativeBehaviors),
          new FrameworkPropertyMetadata(null));
      // A public setter (for the non-existent "Behaviors" property)
      public static void SetBehaviors(Window window, NativeBehaviors behaviors)
      {
         if (window == null){ throw new ArgumentNullException("window"); }
         window.SetValue(NativeBehaviorsProperty, behaviors);
      }
      // The public getter  (for the non-existent "Behaviors" property)
      public static NativeBehaviors GetBehaviors(Window window)
      {  // instead of GetValue, call the accessor!
         return GetNativeBehaviors(window);
      }
      // This dependency property getter makes sure it returns a collection
      private static NativeBehaviors GetNativeBehaviors(Window window)
      {
         if (window == null) { throw new ArgumentNullException("window"); }
         // This is the plain old normal thing:
         var behaviors = (NativeBehaviors)window.GetValue(NativeBehaviorsProperty);
         // Our raison d'être: create a new collection if there isn't one yet
         if (behaviors == null) { behaviors = new NativeBehaviors(window); }

         return behaviors;
      }
 

There’s a little more to it in the actual class source code, I’m glossing over the simplest parts, and leaving out all the XML documentation comments too. We override the CollectionChanged event so that we can hook new Behaviors as they arrive, and we make sure to hook the WndProc whenever the Target Window is set:


protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs nccea)
{
   base.OnCollectionChanged(nccea);
   // design mode bailout because NativeBehaviors don't work in DesignMode
   if (DesignerProperties.GetIsInDesignMode(Target)) { return; }
   // notify new behaviors they are being hooked up, and track their handlers
   if (nccea.Action == NotifyCollectionChangedAction.Add ||
       nccea.Action == NotifyCollectionChangedAction.Replace)
   {
      foreach (NativeBehavior behavior in nccea.NewItems)
      {
         behavior.AddTo(Target);
         Handlers.AddRange(behavior.GetHandlers());
      }
   }

   // notify removed behaviors they are being unhooked, and stop tracking their handlers
   if (nccea.Action == NotifyCollectionChangedAction.Remove ||
       nccea.Action == NotifyCollectionChangedAction.Replace)
   {
      foreach (NativeBehavior behavior in nccea.OldItems)
      {
         behavior.RemoveFrom(Target);
         foreach (var h in behavior.GetHandlers())
         {
            Handlers.Remove(h);
         }
      }
   }
}

// Hook the Window when its added
// but keep only a week reference to it...
public Window Target
{
   get
   {
      if (_owner != null) {
         return _owner.Target as Window;
      } else return null;
   }
   set
   {
      // design mode bailout (in Design mode there's no window, and no wndproc)
      if (DesignerProperties.GetIsInDesignMode(value)) { return; }

      if (_owner != null && WindowHandle != IntPtr.Zero)
      {
         HwndSource.FromHwnd(WindowHandle).RemoveHook(WndProc);
      }

      Debug.Assert(null != value);
      _owner = new WeakReference(value);

      // Check for an HWND to determine if the Window has been loaded.
      WindowHandle = new WindowInteropHelper(value).Handle;

      if (IntPtr.Zero == WindowHandle)
      { // If there's no handle, set the hook when the Source is initialised.
         value.SourceInitialized += (sender, e) =>
         {
            WindowHandle = new WindowInteropHelper((Window)sender).Handle;
            // hook the WndProc
            HwndSource.FromHwnd(WindowHandle).AddHook(WndProc);
         };
      }
      else
      { // hook the WndProc
         HwndSource.FromHwnd(WindowHandle).AddHook(WndProc);
      }
   }
}
// the weak reference to the actual window...
private WeakReference _owner;
 

And finally, the last part of the puzzle is my actual WndProc implementation, which has a minor drawback in that we can hypothetically have multiple behaviors which process the same window message … and may each return a different value (which we cannot reconcile). For now I’m just returning the last (non-zero) result (in actuality, all of the behaviors I’ve written thus far return zero).


private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
   Debug.Assert(hwnd == WindowHandle); // Only expecting messages for our cached HWND.

   // cast and cache the message
   var message = (NativeMethods.WindowMessage)msg;
   // NOTE: we may process a message multiple times
   // and we have no good way to handle that...
   var result = IntPtr.Zero;
   foreach (var handlePair in Handlers)
   {  if (handlePair.Key == message)
      {
         var r = handlePair.Value(wParam, lParam, ref handled);
         // So, we'll return the last non-zero result (if any)
         if (r != IntPtr.Zero) { result = r; }
   }  }
   return result;
}
 

That pretty much covers the framework. The actual behaviors can be fairly simple like my Snap-To behavior (which is actually too simple), or extremely complex like my latest behavior, which is a port Joe Castro’s custom Window ‘Chrome’ to my behavior framework, and I’m hoping some of you will choose to write new ones and submit them as patches to the CodePlex project (for now, I’m just piggybacking it on the PoshConsole project). But in any case, the latest code is available via subversion on CodePlex, and you can download today’s snapshot here.

Reblog this post [with Zemanta]
Search My Content