WinRT/Xaml/AKA Metro DataTemplate selection based on Data Types

You may have noticed that WinRT does not have automatic resolution of a DataTemplate based on the data type of object added to an ItemsControl. While unfortunate as this behavior is quite handy, it’s not too difficult to replicate the functionality using a DataTemplateSelector.

WPF for example, could do something like this:

<DataTemplate DataType="{x:Type local:Task}">
<StackPanel>
<TextBlock Text="{Binding Path=TaskName}" />
<TextBlock Text="{Binding Path=Description}"/>
<TextBlock Text="{Binding Path=Priority}"/>
</StackPanel>
</DataTemplate>

When the Task type as shown above was found in a list, it would have been rendered as a StackPanel with three TextBlocks automatically. That rocked.

WinRT (Metro/Xaml, Windows 8 applications) are missing the DataType property of DataTemplates. Yes, some of you might say it’s not missing as it’s V1, but given the heritage of Windows 8 Xaml applications, I consider it missing.

While an exact duplicate of the functionality isn’t possible, it’s relatively simple to get close.

<GridView
    x:Name="itemGridView"
    ItemsSource="{Binding Source={StaticResource groupedItemsViewSource}}"
    ItemTemplateSelector="{StaticResource typedTemplateSelector}"
    SelectionMode="None"
    IsItemClickEnabled="True">

Take the GridView above for example (using the template project in Visual Studio 2012).

I’ve assigned the ItemTemplateSelector to an instance of the TypedTemplateSelector class I’ve created.

In the Resources for the Page, I added a custom DataTemplate:

image

I’ve added a DataTemplate with a Key called Type : SampleDataItem (without the spaces though – they’re auto converted by my WordPress theme to a squiggle face: Confused smile).

There’s nothing special about the Template, just the name. It must start with Type:.

Here the custom template selector is being constructed in the Resources:

    <local:TypedTemplateSelector x:Key="typedTemplateSelector"
                                 DefaultTemplateKey="Standard250x250ItemTemplate" />
</Page.Resources>

Here’s the class that you’d need below. It has a caching feature (IsCacheEnabled) to prevent the search from occurring more than once for a key. It’s set to True by default. When used, the object searches from the current Item through all Parents trying to match the type of the object (via the VisualTreeHelper). Only the ClassName is used as programmed below (and not the full namespace). You could easily change this behavior by removing the Split and Last calls.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;

namespace WiredPrairie.TemplateSelector
{
    public class TypedTemplateSelector : DataTemplateSelector
    {
        private Dictionary<string, DataTemplate> _cachedDataTemplates;

        /// <summary>
        /// Fallback value for DataTemplate
        /// </summary>
        public string DefaultTemplateKey { get; set; }

        /// <summary>
        /// Cache search results for a type (defaults to Enabled)
        /// </summary>
        public bool IsCacheEnabled { get; set; }

        public TypedTemplateSelector()
        {
            IsCacheEnabled = true;
        }

        protected override Windows.UI.Xaml.DataTemplate SelectTemplateCore(object item, Windows.UI.Xaml.DependencyObject container)
        {
            // grab the Type name. Type will be searched as Type:NAME as shown below
            /*
                <DataTemplate x:Key="Type:SampleDataItem">
                    <Grid HorizontalAlignment="Left" Width="250" Height="250">
                        <TextBlock Text="{Binding Title}" />
                    </Grid>
                </DataTemplate>
             */
            string key = item != null ? string.Format("Type:{0}", item.GetType().Name.Split('.').Last()) : DefaultTemplateKey;
            DataTemplate dt = GetCachedDataTemplate(key);
            try
            {
                if (dt != null) { return dt; }

                // look at all parents (visual parents)
                FrameworkElement fe = container as FrameworkElement;
                while (fe != null)
                {
                    dt = FindTemplate(fe, key);
                    if (dt != null) { return dt; }
                    // if you were to just look at logical parents,
                    // you'd find that there isn't a Parent for Items set
                    fe = VisualTreeHelper.GetParent(fe) as FrameworkElement;
                }

                dt = FindTemplate(null, key);
                return dt;
            }
            finally
            {
                if (dt != null)
                {
                    AddCachedDataTemplate(key, dt);
                }
            }
        }

        private DataTemplate GetCachedDataTemplate(string key)
        {
            if (!IsCacheEnabled) { return null; }
            VerifyCachedDataTemplateStorage();
            if (_cachedDataTemplates.ContainsKey(key))
            {
                return _cachedDataTemplates[key];
            }

            return null;
        }

        private void AddCachedDataTemplate(string key, DataTemplate dt)
        {
            if (!IsCacheEnabled) { return; }
            VerifyCachedDataTemplateStorage();
            _cachedDataTemplates[key] = dt;
        }

        /// <summary>
        /// Delay creates storage
        /// </summary>
        private void VerifyCachedDataTemplateStorage()
        {
            if (_cachedDataTemplates == null)
            {
                _cachedDataTemplates = new Dictionary<string, DataTemplate>();
            }

        }

        /// <summary>
        /// Returns a template
        /// </summary>
        /// <param name="source">Pass null to search entire app</param>
        /// <param name="key"></param>
        /// <returns></returns>
        private static DataTemplate FindTemplate(object source, string key)
        {
            var fe = source as FrameworkElement;
            object obj;
            ResourceDictionary rd = fe != null ? fe.Resources : App.Current.Resources;
            if (rd.TryGetValue(key, out obj))
            {
                DataTemplate dt = obj as DataTemplate;
                if (dt != null)
                {
                    return dt;
                }
            }
            return null;

        }
    }
}

3 Comments

  1. The article was really nice until you created the TypedTemplateSelector class. I’m only 2 chapters ahead of a newbie….I was following you OK…because I noticed the same issue in Windows 8 Apps…While searching..I wound up here. Anyway…good job.

Comments are closed.