Creating an “Add New” Item Template in a Windows 8 App ListView

A common UI pattern is to have a list of things on screen and at the end of the list have a plus icon (or something) that triggers your “add new item” code.

For example, in the Weather app:

cdt_weatherapp

For example, say we want to display a list of friends in a XAML ListView and at the bottom of the list have a “+” that adds a new friend.

public class Friend
{
    public string Name { get; set; }
}

The first step is to enable us to differentiate between what is an actual friend in the list and what is the “add new placeholder” item.

Rather than binding the ListView directly to a list of Friend, we can create an abstraction.

public abstract class FriendListItemBase
{
}

public class FriendListItem : FriendListItemBase
{
    public Friend Friend{ get; set; }
}

public class AddFriendPlaceHolderListItem : FriendListItemBase
{
}

Now the ListView is bound to an ObservableCollection<FriendListItemBase>. We create some initial sample data:

private void CreateSampleData()
{
    FriendViewModels = new ObservableCollection<FriendListItemBase>
                           {
                               new FriendListItem {Friend = new Friend {Name = "Plato"}},
                               new FriendListItem {Friend = new Friend {Name = "Joan"}},
                               new FriendListItem {Friend = new Friend {Name = "Al"}},
                               new AddFriendPlaceHolderListItem()
                           };
}

Where:

public ObservableCollection<FriendListItemBase> FriendViewModels { get; set; }

So now we have a list of “view model items” that can either be a FriendListItem or a AddFriendPlaceHolderListItem. The next thing is to enable the ListView to choose between two different DataTemplates, we can do this by creating a class that derives from DataTemplateSelector:

public class AddNewIdeaDataTemplateSelector : DataTemplateSelector
{
    public DataTemplate FriendItemTemplate { get; set; }
    public DataTemplate AddNewFriendPlaceholdeTemplate { get; set; }


    protected override Windows.UI.Xaml.DataTemplate SelectTemplateCore(object item, Windows.UI.Xaml.DependencyObject container)
    {
        if (item is FriendListItem)
        {
            return FriendItemTemplate;
        }

        return AddNewFriendPlaceholdeTemplate;
    }
}

What this class does is choose between either the DataTemplate that is set in the FriendItemTemplate or the one in the AddNewFriendPlaceholdeTemplate properties. It does this by simply looking at what type of object it is dealing with.

Now in the XAML we need to create an instance of this AddNewIdeaDataTemplateSelector in additional to different data templates that will change what is displayed in the list depending on the type of the current item being bound:

<Page.Resources>
    <DataTemplate x:Key="FriendListItemDataTemplate">
        <Grid>
            <TextBlock HorizontalAlignment="Left" TextWrapping="Wrap" Text="{Binding Friend.Name}" VerticalAlignment="Top"/>
        </Grid>
    </DataTemplate>
    <DataTemplate x:Key="AddNewFriendListItemDataTemplate">
        <Grid>  
            <TextBlock HorizontalAlignment="Left" TextWrapping="Wrap" Text="+" VerticalAlignment="Top" Foreground="Gold" FontSize="40" FontWeight="ExtraBold"/>
        </Grid>
    </DataTemplate>
    <viewModel:AddNewIdeaDataTemplateSelector  x:Key="FriendDataTemplateSelector" 
   FriendItemTemplate="{StaticResource FriendListItemDataTemplate}" 
   AddNewFriendPlaceholdeTemplate="{StaticResource AddNewFriendListItemDataTemplate}"/>
</Page.Resources>

Now the actual ListView can be configured as follows:

<ListView SelectionMode="Single" 
ItemsSource="{Binding FriendViewModels}" 
ItemTemplateSelector="{StaticResource FriendDataTemplateSelector}" 
SelectionChanged="Selector_OnSelectionChanged" 
Background="Gray"/>

Note that instead of the usual ItemTemplate property (that usual points to a single DataTemplate) we are using the ItemTemplateSelector that points to our AddNewIdeaDataTemplateSelector.

Now when a AddNewFriendPlaceholdeTemplate is bound to the AddNewFriendListItemDataTemplate is used to render it:

cdt_unsorted

Note the big yellow “+”.

The next thing to deal with is selection. Both a friend or the “+” item can be selected, in the code we have to differentiate:

private void Selector_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
    if (!e.AddedItems.Any())
    {
        return;
    }

    var selectedItem = e.AddedItems.First();

    var isAddNewSelected = selectedItem.GetType() == typeof (AddFriendPlaceHolderListItem);

    if (isAddNewSelected)
    {
        ShowMessage("Add new selected");
    }
    else // a friend was selected
    {
        var selectedFriend = selectedItem as FriendListItem;
        ShowMessage(selectedFriend.Friend.Name + " selected");
    }
}

When a new friend is added, it should appear before the “+” (assuming a non auto-sorted list), the Insert method of ObservableCollection can be used:

private void AddLee_OnClick(object sender, RoutedEventArgs e)
{
    FriendViewModels.Insert(FriendViewModels.Count -1, new FriendListItem{Friend=new Friend{Name="Lee"}});
}

The final thing to take into account is sorting. Because we have essentially 2 types of things bound to in the list we need to take care of the sorting. The method is not particularly efficient or robust but it demonstrates the example:

private void SortList()
{
    var f = new List<FriendListItemBase>(FriendViewModels);

    f.Sort((x, y) =>
               {
                   const int xLessThanY = -1;
                   const int xequalsY = 0;

                   if (y.GetType() == typeof (AddFriendPlaceHolderListItem))
                   {
                       return xLessThanY;
                   }

                   if (x.GetType() == typeof (FriendListItem) &&
                       y.GetType() == typeof (FriendListItem))
                   {
                       var xFriendName = (x as FriendListItem).Friend.Name;
                       var yFriendName = (y as FriendListItem).Friend.Name;

                       return String.Compare(xFriendName, yFriendName);
                   }

                   return xequalsY;
               });

    FriendViewModels.Clear();

    foreach (var i in f)
    {
        FriendViewModels.Add(i);
    }
}

This method creates an intermediary List and uses an overload of it’s Sort method that allows a delegate to represent the sort logic. In the delegate (lambda), if the second item Y is a AddFriendPlaceHolderListItem then it will always be “more than” X. Failing that, if both X & Y are FriendListItem then just compare the Friend Names. Otherwise just default to both X & Y being equal.

The complete listings for the XAML and code-behind are below:

<Page
    x:Class="ConditionalDataTemplateExample.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:ConditionalDataTemplateExample"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:viewModel="using:ConditionalDataTemplateExample.ViewModel"
    mc:Ignorable="d">
    <Page.Resources>
        <DataTemplate x:Key="FriendListItemDataTemplate">
            <Grid>
                <TextBlock HorizontalAlignment="Left" TextWrapping="Wrap" Text="{Binding Friend.Name}" VerticalAlignment="Top"/>
            </Grid>
        </DataTemplate>
        <DataTemplate x:Key="AddNewFriendListItemDataTemplate">
            <Grid>  
                <TextBlock HorizontalAlignment="Left" TextWrapping="Wrap" Text="+" VerticalAlignment="Top" Foreground="Gold" FontSize="40" FontWeight="ExtraBold"/>
            </Grid>
        </DataTemplate>
        <viewModel:AddNewIdeaDataTemplateSelector  x:Key="FriendDataTemplateSelector" 
       FriendItemTemplate="{StaticResource FriendListItemDataTemplate}" 
       AddNewFriendPlaceholdeTemplate="{StaticResource AddNewFriendListItemDataTemplate}"/>
    </Page.Resources>

    <StackPanel Width="200">
        <ListView SelectionMode="Single" 
                  ItemsSource="{Binding FriendViewModels}" 
                  ItemTemplateSelector="{StaticResource FriendDataTemplateSelector}" 
                  SelectionChanged="Selector_OnSelectionChanged" 
                  Background="Gray"/>
        <TextBlock>Actions</TextBlock>
        <Button Name="Sort" Click="Sort_OnClick">Sort</Button>
        <Button Name="AddLee" Click="AddLee_OnClick">Add Lee</Button>
    </StackPanel>
</Page>

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using ConditionalDataTemplateExample.Model;
using ConditionalDataTemplateExample.ViewModel;
using Windows.UI.Popups;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;

namespace ConditionalDataTemplateExample
{
    public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            InitializeComponent();

            CreateSampleData();

            DataContext = this;
        }


        public ObservableCollection<Friend> Friends { get; set; }
        public ObservableCollection<FriendListItemBase> FriendViewModels { get; set; }

        private void CreateSampleData()
        {
            FriendViewModels = new ObservableCollection<FriendListItemBase>
                                   {
                                       new FriendListItem {Friend = new Friend {Name = "Plato"}},
                                       new FriendListItem {Friend = new Friend {Name = "Joan"}},
                                       new FriendListItem {Friend = new Friend {Name = "Al"}},
                                       new AddFriendPlaceHolderListItem()
                                   };
        }


        private void Selector_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            if (!e.AddedItems.Any())
            {
                return;
            }

            var selectedItem = e.AddedItems.First();

            var isAddNewSelected = selectedItem.GetType() == typeof (AddFriendPlaceHolderListItem);

            if (isAddNewSelected)
            {
                ShowMessage("Add new selected");
            }
            else // a friend was selected
            {
                var selectedFriend = selectedItem as FriendListItem;
                ShowMessage(selectedFriend.Friend.Name + " selected");
            }
        }

        private void ShowMessage(string m)
        {
            var msgDialog = new MessageDialog(m, "A message");

            msgDialog.Commands.Add(new UICommand("OK"));

            msgDialog.ShowAsync();
        }

        private void Sort_OnClick(object sender, RoutedEventArgs e)
        {
            SortList();
        }

        private void SortList()
        {
            var f = new List<FriendListItemBase>(FriendViewModels);

            f.Sort((x, y) =>
                       {
                           const int xLessThanY = -1;
                           const int xequalsY = 0;

                           if (y.GetType() == typeof (AddFriendPlaceHolderListItem))
                           {
                               return xLessThanY;
                           }

                           if (x.GetType() == typeof (FriendListItem) &&
                               y.GetType() == typeof (FriendListItem))
                           {
                               var xFriendName = (x as FriendListItem).Friend.Name;
                               var yFriendName = (y as FriendListItem).Friend.Name;

                               return String.Compare(xFriendName, yFriendName);
                           }

                           return xequalsY;
                       });

            FriendViewModels.Clear();

            foreach (var i in f)
            {
                FriendViewModels.Add(i);
            }
        }

        private void AddLee_OnClick(object sender, RoutedEventArgs e)
        {
            FriendViewModels.Insert(FriendViewModels.Count - 1, new FriendListItem {Friend = new Friend {Name = "Lee"}});
        }
    }
}

 

(If you’re developing a Windows 8 app and you’d like to learn more about how to choose features for version one of your app, etc check out my Designing Windows 8 Apps Pluralsight course.)

Add comment

Loading