среда, 27 ноября 2013 г.

XAML: Event Handlers for Templated Items of Collection Controls

Recently, while fighting my way through XAML required to create a relatively simple UI for a Windows Phone 8 application I have come across some unexpected difficulties. When it comes to hacking markup I prefer to modularize it as much as possible – many of us do. Here we follow the same ideas that govern us when we write imperative code and break it into relatively small and simple functions and classes to assemble them into something bigger on higher levels. The benefits of this approach are well known: better isolation, finer specification of responsibilities and less code duplication. With markup we usually behave worse – the code for pages frequently feels quite monolithic and it is difficult to explicitly split it into parts each of which pursues its own goals. Here XAML gives us ControlTemplate and DataTemplate allowing to improve the state of affairs to some extent. The DataTemplate is primarily used with collection controls so that we can specify the way each element should be displayed. In my specific case the parent control was LongListSelector, which should have displayed a list of more or less complex items.

The most natural way to use LongListSelector (as any other collection control) is to supply it with  items through ItemsSource property and tell how each of them should be displayed – for this purpose one uses the ItemTemplate property.

<!-- LongListSelector with DataTemplate -->

<phone:LongListSelector ItemsSource="{Binding Employees}">
<phone:LongListSelector.ItemTemplate>
<DataTemplate>
    <Grid Background="{StaticResource PhoneAccentBrush}"
             Margin="10,10,10,10" Height="150">
        <Grid.RowDefinitions>
            <RowDefinition Height="0.5*" />
            <RowDefinition Height="0.5*" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="0.5*" />
            <ColumnDefinition Width="0.5*" />
        </Grid.ColumnDefinitions>

        <TextBlock Text="{Binding DepartmentCode}"
            TextAlignment="Right" Margin="10,10,10,10"
            Style="{StaticResource PhoneTextLargeStyle}"
            FontWeight="Bold"
            Grid.Column="1" Grid.Row="0" />

        <TextBlock Text="{Binding Firstname}" Margin="10,10,10,10"
                   Style="{StaticResource PhoneTextLargeStyle}"
                   Grid.Column="0" Grid.Row="1" />
        <TextBlock Text="{Binding Lastname}" Margin="10,10,10,10"
                   Style="{StaticResource PhoneTextLargeStyle}"
                   Grid.Column="1" Grid.Row="1" />
    </Grid>
</DataTemplate>
</phone:LongListSelector.ItemTemplate>
</phone:LongListSelector>

The property expects a DataTemplate instance and in many cases it is pretty OK to specify it right in the place like in the example above. However, if the items are going to show up in the same manner on another screen or two one may want to move the DataTemplate declaration to a separate resource dictionary – quite a natural way to reduce code duplication in XAML. Fortunately, it is very easy to do this, and for the LongListSelector we end up with the code like this:

<!-- DataTemplate from resources -->
<!-- (a bit shorter than the above version, yes) -->

<phone:LongListSelector ItemsSource="{Binding Employees}"
       ItemTemplate="{StaticResource AwesomeEmployeeTemplate}" />

If, like me, you prefer things to be clickable you are going to face a little problem right at the moment you finish moving the template out of the page code. The issue is that one cannot simply specify an event handler for the templated elements of the LongListSelector – it doesn’t provide any events like ItemClick, ItemTap or ItemWhatever. While this somber fact looks pretty logical – this would directly confront the idea that each component should be responsible for its own behavior and its own behavior only, I still want to hook the items of the list to certain events – say, to Tap. Fortunately there are at least two ways to achieve this.

I wanted to say here that it is possible to move the templates to a separate ResourceDictionary and create event handlers for them in the dictionary’s code-behind class. However, a little exploration showed that the MergedDictionaries thing that allows to combine resource files into an easily accessible application or page level dictionary doesn’t play nicely with the dictionaries backed with code. Still, we can at least specify the desired template in the resources section of the App.xaml so that it is visible to all pages. Since App.xaml has its own code-behind class the event handlers can reside there.

<!-- Resource in App.xaml -->

<Application.Resources>
  <!-- some stuff here -->

  <DataTemplate x:Key="HandlingEmployeeTemplate">
    <Grid Background="{StaticResource PhoneAccentBrush}"
            Margin="10,10,10,10" Height="150"
            Tap="Grid_Tap">
          <!-- the layout of the template stays the same as above -->
      </Grid>
  </DataTemplate>
</Application.Resources>

//App.xaml.cs

namespace HandlersInTemplated
{
  public partial class App : Application
  {

    //A lot of important stuff here

    private void Grid_Tap(object sender, GestureEventArgs e)
    {
      var employee = (Employee)((FrameworkElement)sender).DataContext;
      MessageBox.Show(String.Format(
       "Template: I see you tapped {0} {1} from '{2}'.",
        employee.Firstname, employee.Lastname, employee.DepartmentCode));
    }
  }
}

While this approach is possible and absolutely legitimate, there are a couple of things that I don’t like about it. First of all I wanted to separate my templates and other stuff clearly from other parts of the project. Placing them into the App.xaml will definitely move them far enough from pages, but will likely lead to mixing a lot of different things in one place. Moreover, the App’s personal code-behind class is populated with some application-level routines so adding a lot of handlers there won’t look nice at all. Needless to say, if we decide to place even half a dozen templates with event handlers in the app’s ResourceDictionary, we will end up with a total mess of absolutely independent methods in the code-behind class. This doesn’t sound like a good separation of concerns, does it?

On the other side, should it be easy to achieve, I won’t be much happier with the solution involving separate ResourceDictionaries – each with its own code-behind. This idea certainly offers some degree of flexibility, but there are reasons why it doesn't appeal to me. To begin with, we still face the problem of including more than one template, style or whatever else into a dictionary (there isn’t such a thing like a dictionary with a single word in it, right?) When we use code behind for a dictionary this inevitably leads to mixing handlers for different elements in one class – we have agreed that this is no good. What is more important, creating event handlers for templates located in a dictionary (and similarly  in App.xaml) might point to the fact that what we try to create is actually a custom control. Under certain circumstances, it may be better to stick to a ControlTemplate instead of CustomControl or UserControl, but in this case I would definitely use that strange one-word dictionary. Finally, the logic of the interaction with template items might differ from page to page and this makes me want to specify handlers on the page level. Neither the solution with code behind for a resource dictionary, nor the App.xaml approach can give me such an option, but there is one that both lacks the above drawbacks and solves my specific problem.

The trick is to do what we usually do when we can’t solve a problem – that is introduce another level of indirection. As I have said above, LongListSelector.ItemTemplate property expects a DataTemplate to be passed into it. Because DataTemplate itself is quite a flexible thing, we are free to specify almost anything we want there, so let’s provide our LongListSelector with a template containing a single ContentControl. Looks stupid? Well, maybe it is, but stupid things tend to hold much power. In this case the power is that one can both specify handlers for ContentControl and tune its look and feel with a template. That said, in a separate resource file we create a ControlTemplate, which will specify how the elements of the list are presented, while the event handlers, determining the way these items interact with the user and other elements, reside in the page itself. Precisely what I wanted.

<!-- ControlTemplate in Dictionary -->

<ResourceDictionary xmlns="not/real/ns">

    <!-- oil, wood and other resources here -->

    <ControlTemplate x:Key="ControlEmployeeTemplate">
        <Grid Background="{StaticResource PhoneAccentBrush}"
              Margin="10,10,10,10" Height="150">
       <!-- The usual stuff that we had -->            
        </Grid>
    </ControlTemplate>

</ResourceDictionary>

<!-- LongListSelector with ContentControl -->

<phone:LongListSelector ItemsSource="{Binding Employees}">
  <phone:LongListSelector.ItemTemplate>
    <DataTemplate>
      <ContentControl x:Name="EmployeeControl"
   Template="{StaticResource ControlEmployeeTemplate}"
          Tap="EmployeeControl_Tap" />
    </DataTemplate>
  </phone:LongListSelector.ItemTemplate>
</phone:LongListSelector>

//Handlers in page's code-behind

namespace HandlersInTemplated
{
  public partial class SelfServingMainPage
    : PhoneApplicationPage
  {
    public SelfServingMainPage()
    {
      InitializeComponent();
      this.DataContext = new EmployeesViewModel();
    }

    private void EmployeeControl_Tap(object sender, GestureEventArgs e)
    {
      var employee = (Employee)((FrameworkElement)sender).DataContext;
      MessageBox.Show(String.Format("Page: I see you tapped {0} {1} from '{2}'.",
          employee.Firstname, employee.Lastname, employee.DepartmentCode));
    }
  }
}


I can’t say that this approach is elegant or beautiful. Moreover, it doesn’t permit creating handlers for distinct elements of the template – only for the whole templated control. At the same time, in most cases I either don’t want this level of detail or feel ready to take the way of custom controls. Thus I believe it is pretty fine to follow the described pattern whenever one wants to reuse the visual template for items and to make pages handle interaction with them at the same time.

As usual, I will be glad to hear any comments or suggestions regarding the good ways to combine XAML templates with event-handling.

Комментариев нет:

Отправить комментарий