Huddled Masses
You can do more than breathe for free...
Browse: Home / WPF Window “Native” Behavior: The Base Class

WPF Window “Native” Behavior: The Base Class

By Joel 'Jaykul' Bennett on 16-Oct-2008

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]

Similar Posts:

  • WPF Window “Native” Behavior: Metro Window
  • Creating WPF UIs for PowerShell with PowerBoots and Visual Studio WPF Designer
  • The Obligatory ShowUI Clock
  • Using alternate credentials with the FileSystem in PowerShell

Posted in Huddled | Tagged Development, Interop, NativeBehaviors, PInvoke, Windows Presentation Foundation, WndProc, WPF

« Previous Next »

Lijit Search

Tags

.Net .Net 2008 Scripting Games Automation Bugs Design Development Funny Gadgets GeoShell GUI Huddled Masses Internet licensing Microsoft Modules My Software News Personal PInvoke Pipeline Politics PoshCode PoshConsole PowerBoots PowerShell PowerShell Functions PowerTips Rants Recommender Repository Scripting ShowUI Software Solutions Textile Tips User Group UserInterface WalkThrough WebHosting Windows 7 WordPress WPF Xml

About Huddled Masses

This is web site is dedicated to the musings of Joel Bennett (aka Jaykul) about technology, software, software development, the web, and the world.

Any resemblance of the views expressed and the views of my employer, my terminal, or the view out my window are purely coincidental. The resemblance between them and my own views is non-deterministic. The question of the existence of views in the absence of anyone to hold them is left as an exercise for the reader.

P.S.: I occasionally link to things I think are great. When I do, I occasionally find a "referral code" so I can make a little cash. I promise that I don't link to anything just because of that cash (I wouldn't cross the street for the amount of cash those links bring in, never mind write a whole blog post) ... but I do not promise that things I link to will stay great as time passes, nor that you will agree with me about their greatness!

Archives

  • January 2012
  • October 2011
  • August 2011
  • July 2011
  • June 2011
  • March 2011
  • February 2011
  • January 2011
  • November 2010
  • August 2010

Copyright © 2012 Joel Bennett.

Powered by WordPress and Hybrid.