As part of the new Blend3 release, there is a new DLL called "Microsoft.Expression.Interactivity.dll". This DLL is a pretty cool thing, as it formalises a pattern that many WPF developers have probably already used, in their own manner. It basically formalises the "Attached Behaviour" pattern, which was possible prior to this DLL being available by the use of the attached DPs.
If you want to see a good example of Attached Behaviour using just DPs, have a look at Josh Smith's excellent www.codeproject.com article here as it is very good.
As I say, the "Microsoft.Expression.Interactivity.dll", simply standardises this pattern a bit. So how does it all work?
Behaviours
Well, behaviours can be added to any control quite easily. It is just a case of inheriting from the correct baseclass
, and providing a couple of overrides for:
And then you simply need to attach behaviours to the UIElement
(s) that you would like to use the behaviours. In XAML, attaching behaviours looks like this:
1: <Window x:Class="BlendBehaviors.Window1"
2: xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4: xmlns:interactivity="clr-namespace:Microsoft.Expression.Interactivity;
assembly=Microsoft.Expression.Interactivity"
5: xmlns:local="clr-namespace:BlendBehaviors;assembly=" >
6:
7: <Rectangle Width="50" Height="50" Canvas.Left="10" Canvas.Top="10"
8: Fill="Aqua">
9:
10: <interactivity:Interaction.Behaviors>
11: <local:DragBehavior/>
12: <local:ResizeBehavior/>
13: </interactivity:Interaction.Behaviors>
14: </Rectangle>
Can you see that we are able to add behaviours by the use of a new property "Behaviours
". So that’s all cool, so what does one of these behaviours actually look like. Well, there are a couple over at the Expression Blend gallery (http://gallery.expression.microsoft.com/site/items/behaviors), but for now, let's have a look at one that I conjured up using. This one is a Resize Behaviour that when attached to an element will allow it to be resized using a ResizeAdorner
which is hosted in the AdornerLayer
.
ResizeBehavior
1: using System.Windows;
2: using System.Windows.Input;
3: using System.Windows.Media;
4: using Microsoft.Expression.Interactivity;
5: using System.Windows.Documents;
6: using System.Windows.Controls;
7:
8:
9: namespace BlendBehaviors
10: {
11:
12: 13: 14: 15: 16: public class ResizeBehavior : Behavior<UIElement>
17: {
18: #region Data
19: private AdornerLayer adornerLayer;
20: private static Window parent;
21: private FrameworkElement fe;
22: private UIElement attachedElement;
23: #endregion
24:
25: #region Ctor
26: static ResizeBehavior()
27: {
28: parent = Application.Current.MainWindow;
29:
30: }
31: #endregion
32:
33: #region Behaviour Overrides
34:
35: protected override void OnAttached()
36: {
37: attachedElement = this.AssociatedObject;
38: fe = attachedElement as FrameworkElement;
39:
40: if (fe.Parent != null)
41: {
42: (fe.Parent as FrameworkElement).Loaded += ResizeBehaviorParent_Loaded;
43: }
44: }
45:
46: protected override void OnDetaching()
47: {
48: base.OnDetaching();
49: if (adornerLayer != null)
50: {
51: adornerLayer = null;
52: }
53: }
54: #endregion
55:
56: #region Private Methods
57: 58: 59: 60: private void ResizeBehaviorParent_Loaded(object sender, RoutedEventArgs e)
61: {
62: if (adornerLayer == null)
63: adornerLayer = AdornerLayer.GetAdornerLayer(sender as Visual);
64: attachedElement.MouseEnter += AttachedElement_MouseEnter;
65: }
66:
67: 68: 69: 70: private void AttachedElement_MouseEnter(object sender, MouseEventArgs e)
71: {
72: ResizingAdorner resizingAdorner = new ResizingAdorner(sender as UIElement);
73: resizingAdorner.MouseLeave += ResizingAdorner_MouseLeave;
74: adornerLayer.Add(resizingAdorner);
75: }
76:
77: 78: 79: 80: 81: private void ResizingAdorner_MouseLeave(object sender, MouseEventArgs e)
82: {
83: if (sender != null)
84: {
85: adornerLayer.Remove(sender as ResizingAdorner);
86: }
87: }
88: #endregion
89:
90: }
91: }
And the code for the ResizeAdorner
is one of the Microsoft SDK samples, which is as follows:
1: using System;
2: using System.Collections.Generic;
3: using System.Text;
4: using System.Windows;
5: using System.Windows.Controls;
6: using System.Windows.Controls.Primitives;
7: using System.Windows.Documents;
8: using System.Windows.Input;
9: using System.Windows.Media;
10:
11:
12: 13: 14: 15: 16: 17: 18:
19:
20: namespace BlendBehaviors
21: {
22:
23: 24: 25: 26: 27: public class ResizingAdorner : Adorner
28: {
29: #region Data
30:
31:
32: Thumb topLeft, topRight, bottomLeft, bottomRight;
33:
34:
35: VisualCollection visualChildren;
36: #endregion
37:
38: #region Ctor
39:
40: public ResizingAdorner(UIElement adornedElement)
41: : base(adornedElement)
42: {
43: visualChildren = new VisualCollection(this);
44:
45:
46:
47: BuildAdornerCorner(ref topLeft, Cursors.SizeNWSE);
48: BuildAdornerCorner(ref topRight, Cursors.SizeNESW);
49: BuildAdornerCorner(ref bottomLeft, Cursors.SizeNESW);
50: BuildAdornerCorner(ref bottomRight, Cursors.SizeNWSE);
51:
52:
53: bottomLeft.DragDelta += new DragDeltaEventHandler(HandleBottomLeft);
54: bottomRight.DragDelta += new DragDeltaEventHandler(HandleBottomRight);
55: topLeft.DragDelta += new DragDeltaEventHandler(HandleTopLeft);
56: topRight.DragDelta += new DragDeltaEventHandler(HandleTopRight);
57: }
58: #endregion
59:
60: #region Private Methods
61:
62: private void HandleBottomRight(object sender, DragDeltaEventArgs args)
63: {
64: FrameworkElement adornedElement = this.AdornedElement as FrameworkElement;
65: Thumb hitThumb = sender as Thumb;
66:
67: if (adornedElement == null || hitThumb == null) return;
68: FrameworkElement parentElement = adornedElement.Parent as FrameworkElement;
69:
70:
71: EnforceSize(adornedElement);
72:
73:
74:
75: adornedElement.Width = Math.Max(adornedElement.Width + args.HorizontalChange,
76: hitThumb.DesiredSize.Width);
77: adornedElement.Height = Math.Max(args.VerticalChange + adornedElement.Height,
78: hitThumb.DesiredSize.Height);
79: }
80:
81:
82: private void HandleBottomLeft(object sender, DragDeltaEventArgs args)
83: {
84: FrameworkElement adornedElement = AdornedElement as FrameworkElement;
85: Thumb hitThumb = sender as Thumb;
86:
87: if (adornedElement == null || hitThumb == null) return;
88:
89:
90: EnforceSize(adornedElement);
91:
92:
93:
94: adornedElement.Width = Math.Max(adornedElement.Width - args.HorizontalChange,
95: hitThumb.DesiredSize.Width);
96:
97: adornedElement.Height = Math.Max(args.VerticalChange + adornedElement.Height,
98: hitThumb.DesiredSize.Height);
99: }
100:
101:
102: private void HandleTopRight(object sender, DragDeltaEventArgs args)
103: {
104: FrameworkElement adornedElement = this.AdornedElement as FrameworkElement;
105: Thumb hitThumb = sender as Thumb;
106:
107: if (adornedElement == null || hitThumb == null) return;
108: FrameworkElement parentElement = adornedElement.Parent as FrameworkElement;
109:
110:
111: EnforceSize(adornedElement);
112:
113:
114:
115: adornedElement.Width = Math.Max(adornedElement.Width + args.HorizontalChange,
116: hitThumb.DesiredSize.Width);
117:
118: adornedElement.Height = Math.Max(adornedElement.Height - args.VerticalChange,
119: hitThumb.DesiredSize.Height);
120: }
121:
122:
123: private void HandleTopLeft(object sender, DragDeltaEventArgs args)
124: {
125: FrameworkElement adornedElement = AdornedElement as FrameworkElement;
126: Thumb hitThumb = sender as Thumb;
127:
128: if (adornedElement == null || hitThumb == null) return;
129:
130:
131: EnforceSize(adornedElement);
132:
133:
134:
135: adornedElement.Width = Math.Max(adornedElement.Width - args.HorizontalChange,
136: hitThumb.DesiredSize.Width);
137:
138: adornedElement.Height = Math.Max(adornedElement.Height - args.VerticalChange,
139: hitThumb.DesiredSize.Height);
140: }
141:
142:
143:
144: private void BuildAdornerCorner(ref Thumb cornerThumb, Cursor customizedCursor)
145: {
146: if (cornerThumb != null) return;
147:
148: cornerThumb = new Thumb();
149:
150:
151: cornerThumb.Cursor = customizedCursor;
152: cornerThumb.Height = cornerThumb.Width = 10;
153: cornerThumb.Opacity = 0.40;
154: cornerThumb.Background = new SolidColorBrush(Colors.MediumBlue);
155:
156: visualChildren.Add(cornerThumb);
157: }
158:
159:
160:
161:
162:
163: private void EnforceSize(FrameworkElement adornedElement)
164: {
165: if (adornedElement.Width.Equals(Double.NaN))
166: adornedElement.Width = adornedElement.DesiredSize.Width;
167: if (adornedElement.Height.Equals(Double.NaN))
168: adornedElement.Height = adornedElement.DesiredSize.Height;
169:
170: FrameworkElement parent = adornedElement.Parent as FrameworkElement;
171: if (parent != null)
172: {
173: adornedElement.MaxHeight = parent.ActualHeight;
174: adornedElement.MaxWidth = parent.ActualWidth;
175: }
176: }
177: #endregion
178:
179: #region Overrides
180:
181: protected override Size ArrangeOverride(Size finalSize)
182: {
183:
184:
185:
186:
187: double desiredWidth = AdornedElement.DesiredSize.Width;
188: double desiredHeight = AdornedElement.DesiredSize.Height;
189:
190: double adornerWidth = this.DesiredSize.Width;
191: double adornerHeight = this.DesiredSize.Height;
192:
193: topLeft.Arrange(new Rect(-adornerWidth / 2, -adornerHeight / 2,
194: adornerWidth, adornerHeight));
195:
196: topRight.Arrange(new Rect(desiredWidth - adornerWidth / 2,
197: -adornerHeight / 2, adornerWidth, adornerHeight));
198:
199: bottomLeft.Arrange(new Rect(-adornerWidth / 2,
200: desiredHeight - adornerHeight / 2, adornerWidth, adornerHeight));
201:
202: bottomRight.Arrange(new Rect(desiredWidth - adornerWidth / 2,
203: desiredHeight - adornerHeight / 2, adornerWidth, adornerHeight));
204:
205:
206: return finalSize;
207: }
208:
209:
210:
211: protected override int VisualChildrenCount { get { return visualChildren.Count; } }
212: protected override Visual GetVisualChild(int index) { return visualChildren[index]; }
213: #endregion
214: }
215: }
So when we run the attached demo app, we are able to resize any element that has this behaviour attached (ok, I am assuming the element is in a Panel
, etc., but you should get the idea).
DragBehavior
And here is another behaviour (from the Expression blend team), that allows an element to be dragged.
1: using System.Windows;
2: using System.Windows.Input;
3: using System.Windows.Media;
4: using Microsoft.Expression.Interactivity;
5:
6: 7: 8: 9: 10: 11: 12:
13:
14: namespace BlendBehaviors
15: {
16:
17: 18: 19: 20: 21: public class DragBehavior : Behavior<UIElement>
22: {
23: #region Data
24: private bool isDragging = false;
25: private UIElement attachedElement;
26: private Window parent;
27: private Point lastPosition;
28: private TranslateTransform translatePosition;
29: #endregion
30:
31: #region Behaviour Overrides
32: protected override void OnAttached()
33: {
34: attachedElement = this.AssociatedObject;
35: parent = Application.Current.MainWindow;
36:
37: attachedElement.MouseLeftButtonDown += new MouseButtonEventHandler(MouseIsDown);
38: attachedElement.MouseLeftButtonUp += new MouseButtonEventHandler(MouseIsUp);
39: attachedElement.MouseMove += new MouseEventHandler(MouseIsMoving);
40: }
41: #endregion
42:
43: #region Private Methods
44: private void MouseIsMoving(object sender, MouseEventArgs e)
45: {
46: if (isDragging)
47: {
48: Point currentPosition = e.GetPosition(parent);
49:
50: double dX = currentPosition.X - lastPosition.X;
51: double dY = currentPosition.Y - lastPosition.Y;
52:
53: this.lastPosition = currentPosition;
54:
55: Transform oldTransform = attachedElement.RenderTransform;
56: TransformGroup rt = new TransformGroup();
57: TranslateTransform newPos = new TranslateTransform();
58: newPos.X = dX;
59: newPos.Y = dY;
60:
61: translatePosition = newPos;
62: if (oldTransform != null)
63: {
64: rt.Children.Add(oldTransform);
65: }
66: rt.Children.Add(newPos);
67:
68: MatrixTransform mt = new MatrixTransform();
69: mt.Matrix = rt.Value;
70:
71: if (currentPosition.X < 0 || currentPosition.Y < 0)
72: return;
73:
74: attachedElement.RenderTransform = mt;
75: }
76: }
77:
78: private void MouseIsUp(object sender, MouseButtonEventArgs e)
79: {
80: isDragging = false;
81:
82: attachedElement.ReleaseMouseCapture();
83: }
84:
85: private void MouseIsDown(object sender, MouseButtonEventArgs e)
86: {
87: isDragging = true;
88: lastPosition = e.GetPosition(parent);
89: attachedElement.CaptureMouse();
90: }
91: #endregion
92: }
93: }
TargetedTriggerAction(s)
Another nice thing in the "Microsoft.Expression.Interactivity.dll" is TargetedTriggerAction<T>
, these are really cool, and allow an arbitrary action to be performed against an Event
. For example, using these TargetedTriggerAction<T>
, we are easily able to run a Command
when a UIElement.MouseRightButtonUp
occurs. Which previously was quite a task and actually almost would require some dynamically emitted assembly or IL.
Let's check these out next. Firstly in the XAML, we can do something like:
1: <Border CornerRadius="10" Background="WhiteSmoke"
2: Grid.Row="0" BorderBrush="Black" BorderThickness="5">
3:
4: <!– Wire up a CommandAction that will fire an ICommand
5: when the event named by the EventTrigger EventName occurs–>
6: <interactivity:Interaction.Triggers>
7: <interactivity:EventTrigger EventName="MouseRightButtonUp">
8: <local:CommandAction Command="{Binding DemoCommand}"
9: SyncOwnerIsEnabled="True" />
10: </interactivity:EventTrigger>
11: </interactivity:Interaction.Triggers>
12:
13:
14: <TextBlock Text="Right-click to fire demo ViewModel bound command"
15: TextWrapping="Wrap" HorizontalAlignment="Center"
16: VerticalAlignment="Center"/>
17:
18:
19:
20: </Border>
Which as you can see will fire the CommandAction
when the MouseRightButtonUp RoutedEvent
occurs for the Border
in which the EventTrigger
is declared. If we now focus our attention to the CommandAction
implementation (again, this is from the Expression Blend gallery http://gallery.expression.microsoft.com/site/items/behaviors):
1: using System;
2: using System.ComponentModel;
3: using System.Windows;
4: using System.Windows.Input;
5: using Microsoft.Expression.Interactivity;
6:
7:
8: 9: 10: 11: 12: 13: 14:
15: namespace BlendBehaviors
16: {
17:
18:
19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: public class CommandAction :
38: TargetedTriggerAction<FrameworkElement>,
39: ICommandSource
40: {
41: #region DPs
42:
43: #region Command DP
44: 45: 46: 47: 48: 49: [Category("Command Properties")]
50: public ICommand Command
51: {
52: get { return (ICommand)GetValue(CommandProperty); }
53: set { SetValue(CommandProperty, value); }
54: }
55:
56: public static readonly DependencyProperty CommandProperty =
57: DependencyProperty.Register(
58: "Command", typeof(ICommand), typeof(CommandAction),
59: new PropertyMetadata(
60: (ICommand)null, OnCommandChanged));
61:
62: private static void OnCommandChanged(DependencyObject d,
63: DependencyPropertyChangedEventArgs e)
64: {
65: var action = (CommandAction)d;
66: action.OnCommandChanged((ICommand)e.OldValue, (ICommand)e.NewValue);
67: }
68:
69: #region Command implementation
70:
71: 72: 73: 74: 75: 76: private EventHandler CanExecuteChangedHandler;
77:
78:
79:
80: private void OnCommandChanged(ICommand oldCommand, ICommand newCommand)
81: {
82: if (oldCommand != null)
83: UnhookCommand(oldCommand);
84: if (newCommand != null)
85: HookCommand(newCommand);
86: }
87:
88: private void UnhookCommand(ICommand command)
89: {
90: command.CanExecuteChanged -= CanExecuteChangedHandler;
91: UpdateCanExecute();
92: }
93:
94: private void HookCommand(ICommand command)
95: {
96:
97:
98:
99: CanExecuteChangedHandler = new EventHandler(OnCanExecuteChanged);
100: command.CanExecuteChanged += CanExecuteChangedHandler;
101: UpdateCanExecute();
102: }
103:
104: private void OnCanExecuteChanged(object sender, EventArgs e)
105: {
106: UpdateCanExecute();
107: }
108:
109: private void UpdateCanExecute()
110: {
111: if (Command != null)
112: {
113: RoutedCommand command = Command as RoutedCommand;
114: if (command != null)
115: IsEnabled = command.CanExecute(CommandParameter, CommandTarget);
116: else
117: IsEnabled = Command.CanExecute(CommandParameter);
118: if (Target != null && SyncOwnerIsEnabled)
119: Target.IsEnabled = IsEnabled;
120: }
121: }
122:
123: #endregion
124:
125:
126: #endregion
127:
128: #region CommandParameter DP
129: 130: 131: 132: [Category("Command Properties")]
133: public object CommandParameter
134: {
135: get { return (object)GetValue(CommandParameterProperty); }
136: set { SetValue(CommandParameterProperty, value); }
137: }
138:
139: public static readonly DependencyProperty CommandParameterProperty =
140: DependencyProperty.Register(
141: "CommandParameter", typeof(object), typeof(CommandAction),
142: new PropertyMetadata());
143: #endregion
144:
145: #region CommandTarget DP
146: 147: 148: 149: [Category("Command Properties")]
150: public IInputElement CommandTarget
151: {
152: get { return (IInputElement)GetValue(CommandTargetProperty); }
153: set { SetValue(CommandTargetProperty, value); }
154: }
155:
156: public static readonly DependencyProperty CommandTargetProperty =
157: DependencyProperty.Register(
158: "CommandTarget", typeof(IInputElement), typeof(CommandAction),
159: new PropertyMetadata());
160: #endregion
161:
162: #region SyncOwnerIsEnabled DP
163: 164: 165: 166: 167: [Category("Command Properties")]
168: public bool SyncOwnerIsEnabled
169: {
170: get { return (bool)GetValue(SyncOwnerIsEnabledProperty); }
171: set { SetValue(SyncOwnerIsEnabledProperty, value); }
172: }
173:
174: 175: 176: 177: 178: public static readonly DependencyProperty SyncOwnerIsEnabledProperty =
179: DependencyProperty.Register(
180: "SyncOwnerIsEnabled", typeof(bool), typeof(CommandAction),
181: new PropertyMetadata());
182: #endregion
183:
184: #endregion
185:
186: #region overrides
187: 188: 189: 190: 191: 192: protected override void Invoke(object o)
193: {
194: if (Command != null)
195: {
196: var command = Command as RoutedCommand;
197: if (command != null)
198: command.Execute(CommandParameter, CommandTarget);
199: else
200: Command.Execute(CommandParameter);
201: }
202: }
203: #endregion
204: }
205: }
It can be seen that this code is capable of executing a ICommand
when the MouseRightButtonUp
RoutedEvent
occurs for the Border. Neat, huh.
I have wired this up to a demoViewModel
which has a single ICommand
exposed, that is executed when this EventTrigger
/ CommandAction
runs.
1: using System;
2: using System.Collections.Generic;
3: using System.Collections.ObjectModel;
4: using System.IO;
5: using System.Linq;
6: using System.Text;
7: using System.Windows;
8: using System.Xml.Linq;
9: using System.Windows.Input;
10: using System.Linq.Expressions;
11: using System.Windows.Threading;
12: using System.Threading;
13: using System.Diagnostics;
14:
15: namespace BlendBehaviors
16: {
17:
18:
19:
20: 21: 22: 23: 24: 25: 26: public class DemoViewModel : ViewModelBase
27: {
28: #region Data
29:
30: private ICommand demoCommand = null;
31:
32: #endregion
33:
34: #region Ctor
35: public DemoViewModel()
36: {
37:
38: demoCommand = new SimpleCommand
39: {
40: CanExecuteDelegate = x => true,
41: ExecuteDelegate = x =>
42: {
43: MessageBox.Show("In the ViewModel");
44: }
45: };
46: }
47: #endregion
48:
49: #region Public Properties
50:
51: public ICommand DemoCommand
52: {
53: get { return demoCommand; }
54: }
55: #endregion
56: }
57: }
Here is a small demo app with all this good stuff in it.