Xceed just released version 1 of their DataGrid. We're super excited here, as it would mean not having to invest in writting our own for the functionality we need. We'll have to see if it's flexible enough to do what we want. Stay tuned!
Friday, 26 January 2007
Thursday, 25 January 2007
Attached Events By Example - Adding an Activate event to any Selector element (ListView, ListBox, TreeView, etc...)
I couldn't resist using a long title. I've had this article on the back of my mind for a while but only managed to get in the mood for a big writing session tonight, after a glass of Stella and a soapy bath. Go figure.
A common struggle with ListViews, TreeViews and other controls is the lack of an ItemActivate event like the one found in WinForms. Several solutions have been offered, most of them involving inheriting from these controls to add the missing functionality. In the spirit of Windows Presentation Foundation's (oh, Avalon, where art thou...) emphasis on composition, I thought I'd offer a generalized solution for any Selector control, with no need for inheritance, using a nearly unknown little gem called attached events.
Attached events are not much talked about (Nick mentions them in passing), documented (msdn mentions them in one paragraph), or printed about (as far as I can read, neither the Petzold nor Chris & Ian's book talk about them, although Ian mentioned to me it will be in the next version). So what is an attached event, and what is not? Let's start with what it is not.
[Disclaimer: At core, attached events are just routed events used in a different way, and as such are more a pattern than an actual specific piece of technology. And as any pattern, they end up with a fancy name. Naming conventions are based on current msdn documentation, which may (and should) change in the future.]
Qualified Event Names
Often, when people think they use attached events, they in fact use Qualified Event Names. This special syntax lets you attach an event handler for a RoutedEvent anywhere in the tree above the element triggering that event. For example, the following code attaches the Click event triggered by the Button element type, but on its direct parent.
1 <Border Height="50" Width="300" BorderBrush="Gray" BorderThickness="1">
2 <StackPanel Background="LightGray" Orientation="Horizontal" Button.Click="CommonClickHandler">
3 <Button Name="YesButton" Width="Auto" >Yes</Button>
4 <Button Name="NoButton" Width="Auto" >No</Button>
5 <Button Name="CancelButton" Width="Auto" >Cancel</Button>
6 </StackPanel>
7 </Border>
This syntax is the same you'll use for attached events, so it could be said that to add a listener to an attached event, you use the Qualified Event Name notation.
Attached Events
So what are attached events? Let's see what our friends at msdn have to say.
An attached event allows you to attach a handler for a particular event to some child element rather than to the parent that actually defines the event, even though neither the object potentially raising the event nor the destination handling instance define or otherwise "own" that event in their namespace.
If you're like me, it takes a few readings to understand what it means. Let's take it one bit at a time. The first thing to realize is that we're talking about a normal routed event, declared the same way as usual.
public static readonly RoutedEvent ItemActivateEvent =
EventManager.RegisterRoutedEvent("ItemActivate",
RoutingStrategy.Bubble,
typeof(RoutedEventHandler),
typeof(ItemActivation));
Then, it is said attached events are defined on an element. Actually they don't need to be defined on an element that is in your element tree at all, and not even on an element. For example, the Mouse class is a sealed class, and yet it defines all mouse-related attached events. What would be more accurate would be to say that the class defining the event is often neither the user of the event (whoever consumes it by adding a handler on it) nor the source of the event (whatever code raises it). For example, the previous attached event has been defined on my ItemActivation class.
namespace SerialSeb.Windows.Controls
{
public static class ItemActivation
{
...
What is common to all attached events however, is the absence of an event declaration using the add{} and remove{} accessors to call AddHandler and RemoveHandler on the instance of the object. Instead, and I suppose it is by convention, two static methods are defined to achieve the adding and removing of an handler.
public static void AddItemActivateHandler(DependencyObject o, RoutedEventHandler handler)
{
((UIElement)o).AddHandler(ItemActivation.ItemActivateEvent, handler);
}
public static void RemoveItemActivateHandler(DependencyObject o, RoutedEventHandler handler)
{
((UIElement)o).RemoveHandler(ItemActivation.ItemActivateEvent, handler);
}
You'll find that all attached events follow the same AddEventNameHandler and RemoveEventNameHandler convention.
The pieces of the puzzle start falling into place slowly. Of course, now that an event has been defined, you want to consume it. To do so, you want to add a handler for that event, but it's not defined on an element present in your tree. The following XAML code shows how it can be done. Note that you can of course add and remove handlers programmatically through the two methods you've defined.
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:my="clr-namespace:SerialSeb.Windows.Controls">
<Grid my:ItemActivation.ItemActivate="HandleItemActivate">
...
</Grid>
</Window>
I believe it strikes a phenomenal resemblance to the Qualified Event Name notation I mentioned earlier. Now, whenever the routed event ItemActivate we've defined earlier bubbles up from somewhere within the grid, it will be caught just like any normal routed event. After all, as I said before, this is just a normal routed event.
We've seen how you declare an attached event, and how to add a handler for it anywhere on our tree. The big question is now to know who is going to raise it? Like any routed event, any code can raise this event, as long as it knows on which element it wants to start bubbling it.
Raising an Attached Event
Raising our ItemActivate event needs to be done whenever, within a Selector, a user double-click on an item, or presses the Enter key after selecting one. To do so, we need to attach some behavior to that element, and we're going to use Dan Crevier's excellent attached property trick.
Whenever you declare an attached property, you can define a PropertyChangedCallback that will get called whenever the value of the property changes, and that includes the first time it's applied. And you get a reference to the element it's applied to, absolutely perfect to hook our behavior code to the Selector element!
Let's start by defining the attached property in our ItemActivation class.
public static ActivationMode GetActivationMode(DependencyObject obj)
{
return (ActivationMode)obj.GetValue(ActivationModeProperty);
}
public static void SetActivationMode(DependencyObject obj, ActivationMode value)
{
obj.SetValue(ActivationModeProperty, value);
}
public static readonly DependencyProperty ActivationModeProperty =
DependencyProperty.RegisterAttached("ActivationMode",
typeof(ActivationMode),
typeof(ItemActivation),
new FrameworkPropertyMetadata(ActivationMode.None,
ItemActivation.HandleActivationModeChanged));
Overall a very simple attached property called ActivationMode. The type is a simple enumeration defining what can trigger the raising of our event, Mouse, Keyboard or both. And finally, a call to our HandleActivationModeChanged static method that will provide for the subscription to the events we're interested in.
private static MouseButtonEventHandler SelectorMouseDoubleClickHandler = new MouseButtonEventHandler(ItemActivation.HandleSelectorMouseDoubleClick);
private static KeyEventHandler SelectorKeyDownHandler = new KeyEventHandler(ItemActivation.HandleSelectorKeyDown);
private static void HandleActivationModeChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
{
Selector selector = target as Selector;
if (target == null) // if trying to attach to something else than a Selector, just ignore
return;
ActivationMode newActivation = (ActivationMode)e.NewValue;
if ((newActivation & ActivationMode.Mouse) == ActivationMode.Mouse)
{
selector.MouseDoubleClick += SelectorMouseDoubleClickHandler;
}
if ((newActivation & ActivationMode.Keyboard) == ActivationMode.Keyboard)
{
selector.KeyDown += SelectorKeyDownHandler;
}
else
{
selector.KeyDown -= SelectorKeyDownHandler;
selector.MouseDoubleClick -= SelectorMouseDoubleClickHandler;
}
}
I defined two handlers for the KeyDown and the MouseDoubleClick events of our Selector. Only thing left to do is to raise our event when the user double-clicked on something.
static void HandleSelectorMouseDoubleClick(object o, MouseButtonEventArgs e)
{
ItemsControl sender = o as ItemsControl;
DependencyObject originalSender = e.OriginalSource as DependencyObject;
if (sender == null || originalSender == null) return;
DependencyObject container = ItemsControl.ContainerFromElement(sender as ItemsControl, e.OriginalSource as DependencyObject);
// just in case, check if the double click doesn't come from somewhere else than something in a container
if (container == null || container == DependencyProperty.UnsetValue) return;
// found a container, now find the item.
object activatedItem = sender.ItemContainerGenerator.ItemFromContainer(container);
if (activatedItem != null && activatedItem != DependencyProperty.UnsetValue)
sender.RaiseEvent(new ItemActivateEventArgs(ItemActivation.ItemActivateEvent, sender, activatedItem, ActivationMode.Mouse));
}
We get the container (the one returned by the ItemsControl.ItemTemplate property), from which we can get the Item that is being represented by this fragment of the tree.
And there you have it, we raise our attached event by calling the RaiseEvent method of our target element! Our attached event is now going to bubble from the Selector and you'll be able to catch it wherever you want in your tree.
Conclusion
We now have an ItemActivate routed event that can be used on any selector, by using attached properties and attached events. Through composition we've added behavior and functionality to a type without having to inherit from it.
Next time, we'll learn how to bind a RoutedEvent to a Command anywhere in your XAML. Stay tuned...
P.S. I'll post the complete code, with keyboard support, and tunneling PreviewItemActivate event when I find the time to set-up my other web sites. In the meantime, don't hesitate to copy and paste!
Wednesday, 24 January 2007
You know how it is sometimes...
.. you download stuff and you forget to even install it. I did that today with StyleSnooper. Added to my toolbox!
Thursday, 18 January 2007
Going through my WPF backblog...
I use this post as a bookmark as much for myself as for others.
Mike Hillberg talks about the Loaded and Initialized event, as well as about Trace sources in WPF.
How to show different text based on an enum value?
This question was asked on the MSDN forums, so I thought I'd replciate it here for everybody's benefit.
When you want to bind to a value that's an enumeration, sometimes you want to show specific text. While writting a converter is one way of solving this problem, it requires writting code. And no code is of better quality than the one you don't write.
So here's the sample I posted.
MainWindow.xaml:
1 <Window x:Class="MsdnForums.TextBlockBoundToEnum.MainWindow"
2 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4 xmlns:local="clr-namespace:MsdnForums.TextBlockBoundToEnum"
5 Title="TextBlockBoundToEnum" Height="300" Width="300"
6 >
7 <!-- Forums: http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=1107413&SiteID=1 -->
8 <DockPanel>
9 <StackPanel DockPanel.Dock="Right">
10 <TextBlock>Current CurState value:</TextBlock>
11 <Button Click="HandleGoodButtonClick">Good</Button>
12 <Button Click="HandleBadButtonClick">Bad</Button>
13 <Button Click="HandleUglyButtonClick">Ugly</Button>
14 </StackPanel>
15 <TextBlock>
16 <TextBlock.Style>
17 <Style TargetType="{x:Type TextBlock}">
18 <Style.Triggers>
19 <DataTrigger Binding="{Binding Path=CurState}" Value="Good">
20 <Setter Property="TextBlock.Text" Value="That was a good one!" />
21 </DataTrigger>
22 <DataTrigger Binding="{Binding Path=CurState}" Value="Bad">
23 <Setter Property="TextBlock.Text" Value="That's no good!" />
24 </DataTrigger>
25 <DataTrigger Binding="{Binding Path=CurState}" Value="Ugly">
26 <Setter Property="TextBlock.Text" Value="Ooooooooooh look at hiiiim!" />
27 </DataTrigger>
28 </Style.Triggers>
29 </Style>
30 </TextBlock.Style>
31 </TextBlock>
32 </DockPanel>
33 </Window>
MainWindow.cs
1 using System;
2 using System.Windows;
3
4 namespace MsdnForums.TextBlockBoundToEnum
5 {
6 public partial class MainWindow : System.Windows.Window
7 {
8 private MyDataContext myDataContext;
9 public MainWindow()
10 {
11 InitializeComponent();
12 this.DataContext = myDataContext = new MyDataContext();
13 }
14 public void HandleGoodButtonClick(object source, RoutedEventArgs e)
15 {
16 myDataContext.CurState = APP_STATE.Good;
17 }
18 public void HandleBadButtonClick(object source, RoutedEventArgs e)
19 {
20 myDataContext.CurState = APP_STATE.Bad;
21 }
22 public void HandleUglyButtonClick(object source, RoutedEventArgs e)
23 {
24 myDataContext.CurState = APP_STATE.Ugly;
25 }
26 }
27 public class MyDataContext : DependencyObject
28 {
29 public APP_STATE CurState
30 {
31 get { return (APP_STATE)GetValue(CurStateProperty); }
32 set { SetValue(CurStateProperty, value); }
33 }
34 public static readonly DependencyProperty CurStateProperty =
35 DependencyProperty.Register("CurState", typeof(APP_STATE), typeof(MyDataContext), new UIPropertyMetadata(APP_STATE.Good));
36 }
37 public enum APP_STATE
38 {
39 Good,
40 Bad,
41 Ugly
42 }
43 }
To profile your WPF applications
Tim Cahill talks about profiling your WPF application. Not a new post but one worth knowing, bookmarking and using!
Monday, 15 January 2007
Blog or forum...
I've not been very active updating this blog. First, I'm in the process of setting up another blog that would support some stuff I have pending, and on my own domain name. Plus it would be nice to re-upload my old archives of entries and without controlling the code it's gonna be hard.
Second issue for me, and the major one, is that my time is extremely limited as we're all working very hard on the project we're working on at the moment (WPF of course). And it comes down to a choice to make between my blog and my contributions in the msdn forum.
So for now, you can find me on the msdn wpf forum. I genuinely think my time is better spent there for the next few weeks, until the new blog arrives!
Wednesday, 3 January 2007
Using winmerge as a merge tool in Visual Studio Team Suite
Found in msdn, to change the diff/merge tool used on conflicts with TFS:
Go to Tools > Options > Source Control > Visual Studio Team Foundation Server > Configure User Tools...
Add a Compare pointing to winmergeU.exe and using:
/e /x /s /wl /dl %6 /dr %7 %1 %2
as a command-line argument.
Repeat the operation for merge, this time using:
/e /s /x /ub /dl %6 /dr %7 %1 %2 %4
Enjoy!