Saturday, October 08, 2011

NGTweet Part 16–Implicit Data Templates

In the 9th part of this NGTweet Silverlight learning series I had demonstrated the use of Data Triggers along with Microsoft.Expressions.Interaction dll. In that post I had applied different settings to the properties of the Images based on whether a tweet was normal tweet or a retweet. Although the code was doing what was expected, I personally felt it was too complicated to achieve such a small result. That was in Silverlight 4. In Silverlight 5 we have support for Implicit Data Templates. This is not to be mistaken with Implicit Styles covered in part 13.

How to use Implicit Styles in Silverlight 5

We already have a use case of Implicit Data Templates. We have two types of tweets in the NGTweet application. The basic one is the normal tweet and the other one is the Retweet. We display the retweet with the distinction that the original profile image is blurred and the retweeted user’s profile image is overlaid on top of it. Also the screen name of the user is displayed differently in case of retweet. The images sizes are also different  for the profile image in case of retweet.

The way Implicit Data Templates work is very much similar to that of Implicit Styles. Implicit styles are applicable to controls like Textbox, TextBlock etc. Likewise we can define a Data Template for a class. When that class is used in the application, it will automatically get the Data Template associated with it. In order to understand it better, lets look at the code we had used in Part 9.

                <i:Interaction.Triggers>
                    <ei:DataTrigger Binding="{Binding Path=IsRetweet}"
                                   Value="True">
                        <ei:ChangePropertyAction TargetObject="{Binding ElementName=ProfileImage}"
                                                TargetName="Height"
                                                Value="30"
                                                PropertyName="Height" />
                        <ei:ChangePropertyAction TargetObject="{Binding ElementName=ProfileImage}"
                                                TargetName="Width"
                                                Value="30"
                                                PropertyName="Width" />
                        <ei:ChangePropertyAction TargetObject="{Binding ElementName=ProfileImage}"
                                                TargetName="Margin"
                                                Value="15, 40, 10, 10"
                                                PropertyName="Margin" />
                        <ei:ChangePropertyAction TargetObject="{Binding ElementName=ProfileImage}"
                                                TargetName="VerticalAlignment"
                                                Value="Top"
                                                PropertyName="VerticalAlignment" />
                        <ei:ChangePropertyAction TargetObject="{Binding ElementName=ProfileImage}"
                                                TargetName="HorizontalAlignment"
                                                Value="Left"
                                                PropertyName="HorizontalAlignment" />

                        <ei:ChangePropertyAction TargetObject="{Binding ElementName=RoundedCornerRectangle}"
                                                TargetName="Rect"
                                                Value="0,0,30,30"
                                                PropertyName="Rect" />
                    </ei:DataTrigger
>

As we can see from the above snippet, we are setting different values to properties like Height and Width of the profile image based on the value of IsRetweet property from the view model. I have copied only the piece related to settings when IsRetweet is true. We have similar settings with different values when the value is false. One problem I feel with the above code is that its not the natural XAML syntax. We’ll refactor this piece of code into a manageable chunk using implicit data template.


In the above piece of code, the distinction is based on whether the tweet is normal or a retweet. Currently our TweeterStatusViewModel is the one deciding whether to show the ScreenName as only the profile name or a formatted name. Also a similar logic exists to populate the OriginalProfileImage property. In case of normal tweet, the OriginalProfileImage property is not applicable.


In its current context the TweeterStatusViewModel violates the Single Responsibility Principle (SRP). It has multiple reasons to change. Lets refactor this a bit. We’ll do some cleanup and rename the TweeterStatusViewModel as TweetViewModel to represent a basic tweet. And add another class RetweetViewModel which extends the TweetViewModel. The properties like Tweet, CreatedDate and Id are common to both Tweet and ReTweet. The ScreenName is applicable to both but is displayed differently. Lets look at the definition of ScreenName in the base class TweetViewModel



        public virtual string ScreenName


        {


            get


            {


                return TweeterStatus.User.ScreenName;


            }


        }


We define it as a virtual property so that the derived class can provide its own implementation. Lets look at the implementation of the derived class RetweetViewModel.



    public class RetweetViewModel : TweetViewModel


    {


        public RetweetViewModel(NGTweeterStatus tweeterStatus)


            : base(tweeterStatus)


        {


        }


 


        public string OriginalProfileImageSource


        {


            get


            {


                return TweeterStatus.RetweetedStatus.User.ProfileImageUrl;


            }


        }


 


        public override string ScreenName


        {


            get


            {


                return string.Format(


                               "{0}, (RT by {1})",


                               TweeterStatus.RetweetedStatus.User.ScreenName,


                               TweeterStatus.User.ScreenName);


            }


        }


    }


As we can see from the above code, this class contains only two properties. The OriginalProfileImageSource returns the respective image URL. Note that we did not have to do a null check to see if the RetweetedStatus is null. The code looks much cleaner this way. Similarly for the ScreenName, we override it to return the formatted string. Once again we don’t do the null check like we used to do in Part 9.


With this refactoring in place we need to also modify the logic for populating the correct view model. This needs to happen when we add the view model to the list. Based on whether the RetweetedStatus is null or not we instantiate the correct view model type as shown below



        public void AddNewTweets(ObservableCollection<NGTweeterStatus> tweeterStatuses)


        {


            tweeterStatuses.Reverse();


            DispatcherHelper.CheckBeginInvokeOnUI(


                () => tweeterStatuses.OrderBy(ts => ts.CreatedDate).ToList().ForEach(


                    t =>


                    {


                        if (t.RetweetedStatus == null)


                            _tweeterStatusViewModels.Insert(0, new TweetViewModel(t));


                        else


                        {


                            _tweeterStatusViewModels.Insert(0, new RetweetViewModel(t));


                        }


                    }));


        }


Now its time to modify the data template. In our previous examples we had defined a data template and applied it to the ItemTemplate of the list box. To use an implicit data template, we follow the same process that we used for implicit styles. We create a data template but do not assign a Key to it. Instead we specify the DataType property to specify which data type will use the specific template. Lets define the Imaplict Data Template for TweetViewModel.

    <DataTemplate DataType="model:TweetViewModel">

        <Border Style="{StaticResource ThickBorderStyle}">

            <Grid>

                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto" />
                    <ColumnDefinition Width="60*" />
                    <ColumnDefinition Width="20*" />
                </Grid.ColumnDefinitions>

                <Grid.RowDefinitions>
                    <RowDefinition />
                    <RowDefinition />
                    <RowDefinition />
                </Grid.RowDefinitions>

                <Image Source="{Binding ProfileImageSource, Mode=OneTime}"
                      x:Name="ProfileImage"
                      Margin="5"
                      Height="50"
                      Width="50"
                      HorizontalAlignment="Center"
                      VerticalAlignment="Top"
                      Grid.RowSpan="2"
                      Grid.Row="0"
                      Grid.Column="0">
                    <Image.Clip>
                        <RectangleGeometry Rect="0,0,50,50"
                                          RadiusX="5"
                                          RadiusY="5" />
                    </Image.Clip>

                </Image>

                <TextBlock Text="{Binding ScreenName, Mode=OneTime}"
                          TextTrimming="WordEllipsis"
                          FontWeight="Bold"
                          Grid.Row="0"
                          Grid.Column="1">
                           <ToolTipService.ToolTip>
                                <ToolTip Content="{Binding ScreenName, Mode=OneTime}" />
                           </ToolTipService.ToolTip>

                </TextBlock>

                <TextBlock Text="{Binding CreatedDate, Mode=OneWay, Converter={StaticResource dateTimeToRelativeTimeConvertor}}"
                          TextAlignment="Right"
                          FontWeight="Thin"
                          FontStyle="Italic"
                          FontSize="10"
                          Grid.Row="0"
                          Grid.Column="2" />

                <TextBlock Text="{Binding Tweet, Mode=OneTime}"
                          TextWrapping="Wrap"
                          VerticalAlignment="Top"
                          Grid.Row="1"
                          Grid.Column="1"
                          Grid.ColumnSpan="2"
                          Margin="2, 0, 2, 2" />
                <StackPanel Grid.Row="2"
                           Grid.Column="1"
                           Grid.ColumnSpan="3">
                    <Button Command="{Binding RetweetCommand, Mode=OneTime}"
                           Height="20"
                           Width="20">
                        <Button.Content>
                            <Image Source="../Assets/Images/retweet.png"
                                  Height="16"
                                  Width="16" />
                        </Button.Content>
                    </Button>
                </StackPanel>
            </Grid>

        </Border>
    </DataTemplate
>

Note the first line of the definition above. Because our view models reside in different assembly, we add a XAML namespace alias called model. Rest of the template is the standard definition. Its much more cleaner and easier to understand as well as maintain.


Similar to this we have an Implicit Data Template for RetweetViewModel.

    <DataTemplate  DataType="model:RetweetViewModel">

        <Border Style="{StaticResource ThickBorderStyle}">

            <Grid>

                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto" />
                    <ColumnDefinition Width="60*" />
                    <ColumnDefinition Width="20*" />
                </Grid.ColumnDefinitions>

                <Grid.RowDefinitions>
                    <RowDefinition />
                    <RowDefinition />
                    <RowDefinition />
                </Grid.RowDefinitions>

                <Image Source="{Binding OriginalProfileImageSource, Mode=OneTime}"
                      Visibility="{Binding IsRetweet, Converter={StaticResource converter}}"
                      Height="50"
                      Width="50"
                      Margin="5"
                      Grid.RowSpan="2"
                      Grid.Row="0"
                      Grid.Column="0"
                      HorizontalAlignment="Center"
                      VerticalAlignment="Top">
                    <Image.Clip>
                        <RectangleGeometry RadiusX="5"
                                          RadiusY="5"
                                          Rect="0,0,50,50" />
                    </Image.Clip>
                    <Image.Effect>
                        <BlurEffect />
                    </Image.Effect>
                </Image>

                <Image Source="{Binding ProfileImageSource, Mode=OneTime}"
                      x:Name="ProfileImage"
                      Height="30"
                      Width="30"
                      Margin="15, 40, 10, 10"
                      HorizontalAlignment="Left"
                      VerticalAlignment="Top"
                      Grid.RowSpan="2"
                      Grid.Row="0"
                      Grid.Column="0">
                    <Image.Clip>
                        <RectangleGeometry Rect="0,0,30,30"
                                          RadiusX="5"
                                          RadiusY="5" />
                    </Image.Clip>

                </Image>

                <TextBlock Text="{Binding ScreenName, Mode=OneTime}"
                          TextTrimming="WordEllipsis"
                          FontWeight="Bold"
                          Grid.Row="0"
                          Grid.Column="1">
                            <ToolTipService.ToolTip>
                                <ToolTip Content="{Binding ScreenName, Mode=OneTime}" />
                            </ToolTipService.ToolTip>

                </TextBlock>

                <TextBlock Text="{Binding CreatedDate, Mode=OneWay, Converter={StaticResource dateTimeToRelativeTimeConvertor}}"
                          TextAlignment="Right"
                          FontWeight="Thin"
                          FontStyle="Italic"
                          FontSize="10"
                          Grid.Row="0"
                          Grid.Column="2" />

                <TextBlock Text="{Binding Tweet, Mode=OneTime}"
                          TextWrapping="Wrap"
                          VerticalAlignment="Top"
                          Grid.Row="1"
                          Grid.Column="1"
                          Grid.ColumnSpan="2"
                          Margin="2, 0, 2, 2" />
                <StackPanel Grid.Row="2"
                           Grid.Column="1"
                           Grid.ColumnSpan="3">
                    <Button Command="{Binding RetweetCommand, Mode=OneTime}"
                           Height="20"
                           Width="20">
                        <Button.Content>
                            <Image Source="../Assets/Images/retweet.png"
                                  Height="16"
                                  Width="16" />
                        </Button.Content>
                    </Button>
                </StackPanel>
            </Grid>

        </Border>
    </DataTemplate
>

This has the additional markup for displaying the Original profile image. Rest of the markup is almost same with minor changes to the properties of profile image. Now that we have the data templates defined for both the tweet and retweet view models lets look at how to use them in the view. Lets look at the ListBox which displays these items.

                <ListBox Width="400"
                        Height="500"
                        HorizontalAlignment="Center"
                        HorizontalContentAlignment="Stretch"
                        ScrollViewer.HorizontalScrollBarVisibility="Disabled"
                        ItemContainerStyle="{StaticResource ListBoxItemStyle}"
                        ItemTemplate="{StaticResource TimeLineListDataTemplate}"
                        ItemsSource="{Binding TweeterStatusViewModels}"
/>

This has the ItemTemplate specified for the items. Since we want to use the Implicit Data Template defined for TweetViewModel and RetweetViewModel, we need to get rid of the explicit item template as below

                <ListBox Width="400"
                        Height="500"
                        HorizontalAlignment="Center"
                        HorizontalContentAlignment="Stretch"
                        ScrollViewer.HorizontalScrollBarVisibility="Disabled"
                        ItemContainerStyle="{StaticResource ListBoxItemStyle}"
                        ItemsSource="{Binding TweeterStatusViewModels}"
/>

And the final result of this is the same like we had before


NGTweet with implicit data templates


Conclusion


The templates features of Silverlight, WPF and Windows Phone 7 are all the same. With the introduction of Implicit Data Templates in Silverlight 5, the differences between WPF and Silverlight versions has reduced to a great extent. Implicit data templates are very helpful when we have inheritance relationships and there is a need to display the representation differently based on the type of the class. A very common example found in many other blog posts is that of an Employee and a Manager. Employee is the base class and Manager is a variant of that. We need to display Manager differently. On the same lines, I hope this post was helpful in making the use of implicit data templates clear.


As always I have uploaded the complete working solution to Dropbox.


Until next time Happy Programming Smile


Further Reading


Here are some books I recommend related to the topics discussed in this blog post.