Search code examples
c#wpfdatatemplatewpf-style

How to write a DataTemplate with some functionality as a resource


I have some ListBoxes in my application which will contain contacts of a phonebook. I want all these ListBoxes to have a DataTemplate as their ItemsTemplates with some functionality like, edit, remove and view as the code below shows:

<DataTemplate x:Key="ContactItemTemplate">
    <TextBlock Foreground="Black" Background="{StaticResource DataTemplateBackgroundBrush}" Padding="5,10" Margin="4,3">
        <TextBlock.Text>
            <MultiBinding StringFormat="{}{0} {1}">
                <Binding Path="FirstName"/>
                <Binding Path="LastName"/>
            </MultiBinding>
        </TextBlock.Text>
        <TextBlock.ContextMenu>
            <ContextMenu FontFamily="B Yekan">
                <MenuItem Header="Edit" Click="btn_EditContact_Click"/>
                <MenuItem Header="Delete" Click="btn_DeleteContact_Click"/>
                <MenuItem Header="View" Click="btn_EditContact_Click"/>
            </ContextMenu>
        </TextBlock.ContextMenu>
    </TextBlock>
</DataTemplate>

It can't be written as a style in a ResourceDictionary and be added to the controls since eventhandlers can not be presented in ResourceDictionaries. So one way is to copy this template and it's handlers in every Window/Page that have a Contact ListBox like below:

<Page.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary Source="pack://application:,,,/Styles/Controls_Style.xaml"/>
        </ResourceDictionary.MergedDictionaries>
        <DataTemplate x:Key="AttachmentsTemplate">
            <Border Height="150" Width="120" BorderThickness="1" BorderBrush="{StaticResource DefaultBorderBrush}" Margin="2">
                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto"/>
                        <RowDefinition/>
                    </Grid.RowDefinitions>
                    <TextBlock Text="{Binding Title}" HorizontalAlignment="Center" VerticalAlignment="Bottom" TextWrapping="WrapWithOverflow"/>
                    <Image Grid.Row="1" Source="{Binding Image}" Margin="5,0"/>
                </Grid>
            </Border>
        </DataTemplate>
    </ResourceDictionary>
</Page.Resources>

Is there any other way that I can write a DataTemplate with some functionality once and use it any where I want? Should I write a UserControl instead of a DataTemplate?


Solution

  • You can have event handlers in your dictionary, as long as they are part of the dictionary -- i.e, the dictionary has some code behind it. This is analogous to Pages and UserControls; you need to add an x:Class attribute. For example, Styles.xml might look like so:

    <ResourceDictionary
        x:Class="WPF.Styles"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfApplication1">
        <DataTemplate x:Key="ContactItemTemplate" DataType="local:Person">
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*" />
                    <ColumnDefinition Width="Auto" />
                </Grid.ColumnDefinitions>
                <TextBlock
                    Grid.Column="0"
                    Foreground="Black"
                    Background="Yellow"
                    Padding="5,10"
                    Margin="4,3"
                    Text="{Binding Name}"
                    >
                    <TextBlock.ContextMenu>
                        <ContextMenu>
                            <MenuItem Header="Edit!" Click="btn_EditContact_Click"/>
                            <MenuItem Header="Delete!" Click="btn_DeleteContact_Click"/>
                            <MenuItem Header="View!" Click="btn_EditContact_Click"/>
                        </ContextMenu>
                    </TextBlock.ContextMenu>
                </TextBlock>
                <Button
                    Grid.Column="1"
                    Content="Edit"
                    Click="btn_EditContact_Click"/>
            </Grid>
        </DataTemplate>
    </ResourceDictionary>
    

    ...with a corresponding Styles.cs (note the partial):

    public sealed partial class Styles
    {
        private void btn_EditContact_Click(object sender, EventArgs args)
        {
            Debug.WriteLine(args);
        }
    
        private void btn_DeleteContact_Click(object sender, EventArgs args)
        {
            Debug.WriteLine(args);
        }
    }
    

    In my window, something like this:

    <Grid.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="pack://application:,,,/Styles.xaml" />
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Grid.Resources>
    
    ...
    
    <ItemsControl
        ItemsSource="{Binding People}"
        Grid.Row="3"
        ItemTemplate="{StaticResource ContactItemTemplate}" />
    

    For completeness, model object:

    public class Person
    {
        public string Name { get; }
    
        public Person(string name)
        {
            Name = name;
        }
    }
    

    ...and ItemsSource:

    public Person[] People { get; } =
        {
            new Person("Donald Duck"),
            new Person("Mickey Mouse"),
            new Person("Darth Vader"),
        };
    

    In general this works fine -- clicking the button, for example, invokes btn_EdxtContact_Click. Sadly, there's a hitch with your scenario involving the context menu of a TextBox; details and possible workarounds in this post.


    Update: On ContextMenus and templates

    A small piece of additional info: The comments on this article has some discussion on ContextMenu templating that might be illuminating.

    ...and one more TextBox ContextMenu link that might prove useful.