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

No hay comentarios:

Publicar un comentario en la entrada