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.
Комментариев нет:
Отправить комментарий