viernes, 31 de agosto de 2012

Modifying a ControlTemplate in a Style at runtime (programmatically)

My problem this time was modifying a ControlTemplate at runtime. Let me explain: 

I have a WPF Button which I create programmatically and I set a pre-defined Style to it.


Button btn = new Button();
btn.Content = "Category";
btn.Style = this.Resources["catButtonStyle"] as Style;
btn.Click += Button_Click;
myGrid.Children.Add(btn);

This style defines a ControlTemplate with a VisualTree as follows:

ControlTemplate/DockPanel/Border/Grid/TextBlock/ContextMenu

The ContextMenu contains two MenuItems but I wanted to add a third MenuItem to be displayed as a Contextual menu at runtime. 

Here is my Style:
<Style x:Key="catButtonStyle" TargetType="Button">
    <Setter Property="OverridesDefaultStyle" Value="True" />
    <Setter Property="Margin" Value="2" />
    <Setter Property="FontFamily" Value="Verdana" />
    <Setter Property="FontSize" Value="11px" />
    <Setter Property="FontWeight" Value="Bold" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="Button">
                <DockPanel Name="CategoryDockPanel">
                    <Border Name="border" BorderThickness="1" Padding="4,2" BorderBrush="DarkGray" CornerRadius="3" Background="{TemplateBinding Background}">
                        <Grid Name="CategoryGrid">
                            <TextBlock Text="My button TextBlock" Name="block">
                            <!-- Context Menus -->
                            <TextBlock.ContextMenu>
                                <ContextMenu>
                                    <MenuItem Header="Properties" />
                                    <MenuItem Header="Remove"  />
                                </ContextMenu>
                            </TextBlock.ContextMenu>
                            </TextBlock>
                        </Grid>
                    </Border>
                </DockPanel>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsMouseOver" Value="True">
                        <Setter TargetName="border" Property="BorderBrush" Value="#FF4788c8" />
                        <Setter Property="Foreground" Value="#FF4788c8" />
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>
So the main logic I had to follow to that was the following:
  1. Find the target Control inside of my ControlTemplate
    I struggled a little bit with this step because I was not able to Find my target control inside of my ControlTemplate.  Note that it is very important to call the ApplyTemplate() method BEFORE trying to find a Control in a ControlTemplate otherwise you will always get null back. This step I was missing and gave me a headache.

    Once you ApplyTemplate, there are basically two ways of finding controls:

    a) By Control Name:  If you choose this route you need to give a Name to the immediate child of your ControlTemplate, in my case was <DockPanel Name="CategoryControlPanel". This link gives a good explanation on how you can Find Controls in a ControlTemplate: How to: Find ControlTemplate-Generated Elements

    b) By Control Type.  If you don't want to give a Name to your ControlTemplate Controls then you can use a helper method to find VisualChildren, like this: Find all controls in WPF Window by type
  2. Manipulate the content of my target control. This step is trivial
Here is how my code looks like:
                         
MenuItem mi = new MenuItem();
mi.Header = "Add Mapping";
//Important: ApplyTemplate First so our ControlTemplate controls are available
btn.ApplyTemplate();
//Find by Name
DockPanel dp = btn.Template.FindName("CategoryDockPanel",btn) as DockPanel;
//Or Find target control directly by Type
var tmp = btn.FindVisualChildren<TextBlock>().ToList();
if(dp != null)
{
    //Get a list of TextBlock childrens in the Visual Tree
    var tb_list = dp.FindVisualChildren<TextBlock>();
    if(tb_list != null && tb_list.Count() > 0)
    {
        //Get the first TextBlock
        TextBlock tb = tb_list.First();
        if (tb != null && tb.ContextMenu != null)
        {
            //Manipulate Context Menus
            tb.ContextMenu.Items.Add(mi);
        }
    }
}

And now I'm able to see the added MenuItem after right-clicking on that button:



Notice that using the "FindVisualChildren" helper method is a quicker way of finding a control by type. But that will depend on your visual tree. There may be situations where you have multiple controls of the same type and you want to access a specific one. Here is where finding by name could be a good option.

That's it for now. Hope you find this useful.

Adolfo

jueves, 30 de agosto de 2012

How to get selected Checkboxes in a nested ListView

I'm currently working in a big MVVM WPF application similar to Visual Studio using AvalonDock.
One of the Panels needs to display two nested ListViews:


The first level just displays a List Name, and the second level displays the Values associated to that List Name:


For that I used an Expander with Header bound to the ListName property in my ViewModel and for the nested ListView design I used DataTemplates. Adding Checkboxes to my ListView wasn't a big deal, as explained here: wpf-listview-with-gridviewcolumn-and-datatemplate

Now one of my requirements was being able to check single or multiple values with the Space bar. For single value you just check the highlighted row, for multiple values you need to select a row and press Shift key while moving down or up to select other adjacent rows. I ran into few hurdles when I tried to code that functionality which I will mention... In WPF there are MANY ways and approaches to doing things as you may know.

So my initial question was: how to handle the row selection events fired by my inner ListView checkboxes?? If you understand WPF Routing Strategies and Event Handlers you know how events can be propagated upwards (bubbling), downwards (tunneling)  or direct.

*From WPF 4 Unleashed book:

Routing Strategies and Event Handlers
When registered, every routed event chooses one of three routing strategies—the way in
which the event raising travels through the element tree. These strategies are exposed as
values of a RoutingStrategyenumeration:

  • Tunneling—The event is first raised on the root, then on each element down the tree until the source element is reached (or until a handler halts the tunneling by marking the event as handled).
  • Bubbling—The event is first raised on the source element and then on each element up the tree until the root is reached (or until a handler halts the bubbling by marking the event as handled).
  • Direct—Theevent is raised only on the source element. This is the same behavior as a plain .NET event, except that such events can still participate in mechanisms specific to routed events such as event triggers.
Handlers for routed events have a signature matching the pattern for general .NET event handlers: The first parameter is a System.Object typically named sender, and the second parameter (typically named e) is a class that derives from System.EventArgs. The sender parameter passed to a handler is always the element to which the handler was attached.
The parameter is (or derives from) an instance of RoutedEventArgs, a subclass of EventArgs that exposes four useful properties:
  • Source—The element in the logical tree that originally raised the event.
  • OriginalSource—The element in the visual tree that originally raised the event (forexample, the TextBlockorButtonChromechild of a standard Button).
  •  Handled—A Boolean that can be set to trueto mark the event as handled. This is precisely what halts any tunneling or bubbling.
  • RoutedEvent—The actual routed event object (such as Button.ClickEvent), which can be helpful for identifying the raised event when the same handler is used for multiple routed events.


In my case I decided to handle the PreviewKeyDown event fired by my Checkbox control at the parent ListView level through bubbling.

All I did was added an Event handler in my View constructor:
lv_Main.PreviewKeyDown += lv_Main_KeyDown;

And then added my Event Handling Method as follows:

        
private void lv_Main_KeyDown(object sender, KeyEventArgs e)
{
    if(e.Key==Key.Space)
    {
        ListViewItem lvi = e.OriginalSource as ListViewItem;
        if(lvi != null)
        {
            KeyValuePair<int, RefDataValue> kvp;
            RefDataValue val;
            //Get parent ListView
            ListView parent = ItemsControl.ItemsControlFromItemContainer(lvi) as ListView;
            //Check if parent list view has highlighted elements
            if(parent != null && parent.Items.Count > 0)
            {
                //Loop through a list of selected ListViewItems
                foreach (KeyValuePair<int, RefDataValue> kvp in parent.SelectedItems)
                {
                    val = kvp.Value;
                    val.IsChecked.Val = !val.IsChecked.Val;
                }
                return;
            }
        }
    }
}

It took me a while to come up with this approach but now I understand better how events work in WPF and how you can handle them at different levels. Also I would like to point out that it is not trivial how to obtain the parent ListView of a given ListViewItem. Comming from a WinForms world I expected to use something like mylistview.Parent, but that doesn't work here, you need to use:

        
ListView parent = ItemsControl.ItemsControlFromItemContainer(lvi) as ListView;

Of course, if you're not dealing with a nested ListView all you have to do is access the ListView.SelectedItems() directly. In this case I had to handle the key down event triggered by my ListViewItem deep inside the VisualTree, that is what made it a little more complicated.

It is also important to mention that i am NOT using the pure MVVM pattern here, I am manipulating my ViewModel through the code-behind which is something to be avoided when implementing MVVM. I still need to find a MVVM solution for this problem where my ViewModel is automatically updated through direct WPF binding. This post could provide some ideas on how to do that: How to get selected item of a nested listview?

Hope you find this useful