(X) Hide this
    • Login
    • Join
      • Generate New Image
        By clicking 'Register' you accept the terms of use .

Windows Store apps in HTML and XAML: Organize your UI layer in XAML

(1 votes)
Andrea Boschin
>
Andrea Boschin
Joined Nov 17, 2009
Articles:   91
Comments:   9
More Articles
0 comments   /   posted on Sep 09, 2013
Categories:   Windows 8

Tweet

Having a deep knowledge of your toolset, when you start a new application from scratch, does not necessarily mean you are able to develop rapidly and effectively. The biggest problems, infact, do not come from the missing knowledge of a feature, that you can easily fill with a short search on the Internet, but from the grow of complexity of your application when you start putting things together without knowing exactly how you can better organize them to avoid the exponential increase of entropy.

The first and much more important aspect is the architectural pattern, made of a set of classes that collaborate together, separating responsabilities between different actors in the UI layer. Be aware that I'm not speaking about the organization of the application tiers, usually separated in UI, Business Logic and Data but about a separation which is completely internal at the UI layer.

Given that this series is about XAML and HTML, in these two last articles I'd like to compare the ways you can implement this separation of concerns, starting with XAML where the most consolidated pattern is called MVVM and then, in the next article, explain how to achieve a similar result in HTML using some additional library. So let start speaking about a similar example, implemented in both the platforms.

MVVM: anyone don't know?

So, sponsoring MVVM at this point is probably something unuseful, just because everyone embraced XAML is already aware that this pattern is surely the better way to deal with this environment. Model-View-VievModel is able to take advantage of some XAML specific peculiarities without mixing logic with interface.

MVVM is made of three components, the View that you may consider the XAML file, the Model that are your entities. Finally, the ViewModel, as the name suggests is the adapter between your model and the view. Infact, it takes data coming from the lower layers, part of the model, it manipulates them to adapt them to the needs of the view. Also, it receives interactions of the user with the view and reflect them onto the model calling the required services.

The key of the pattern is in separating carefully, both vertically and horizontally, the elements. The first question which you have to answer is "How many views (and viewmodels) I've to create?". When you first start with MVVM answer is probably "one per page". This is far away from the reality because, if it is true that a page is a view, it is completely false that there is only a single view per page. What you have to understand is that each part of the UI that atomically presents data and receives commands is a View and have a ViewModel. If you think at a grid, you may understand that each row has a ViewModel because it presents data from a single entity and receive commands (e.g. delete) related to it. Also if you break your page in different parts, probably it is convenient each part be a view and have a viewmodel. As a result the right answer is: "create as many views as it is convenient to horizontally separate the components of your UI".

A simple example

A common UI pattern in Windows Store is to present an item and a set of related contents. After a navigation you arrive to select an item and together with it the interface presents other items you may be interested, on the basis of a simple rule. Let say you have a page like this, where there's a list of related contents, presented as a gridview, and every time you click an item the currently shown detail changes.

screenshot_08312013_192322

The interface here presented can be easily separated in two parts, that are mostly indipendent each other. The first is the representation of a single item (an instance of PersonName) on the left of the screen. The other is a collection of related PersonName instances. They are presented with a GridView on the righ side. These components works separately except for one single thing that is the selection of an item of the GridView. When the item is clicked the other part loads the new item and changes the labels. So, we can easily think at the two parts like two separated UserControl, and they have an autonomous ViewModel. Here are they added to the page:

   1: <Grid Grid.Row="1">
   2:     <Grid.ColumnDefinitions>
   3:         <ColumnDefinition Width="40*"/>
   4:         <ColumnDefinition Width="60*"/>
   5:     </Grid.ColumnDefinitions>
   6:     <vw:NameDetailView DataContext="{Binding Detail}" Grid.Column="0" Margin="120,80,0,0" />
   7:     <vw:RelatedNamesView DataContext="{Binding Related}" Grid.Column="1" Margin="40,80,0,0" />
   8: </Grid>

Also the Page has its own ViewModel that server as coordinator. It has the responsibility of reading the initially selected item and load the children view models:

   1: public class NameViewModel : ViewModelBase, ILoadable
   2: {
   3:     #region Detail property
   4:  
   5:     private NameDetailViewModel detail;
   6:  
   7:     public NameDetailViewModel Detail
   8:     {
   9:         get { return this.detail; }
  10:         set
  11:         {
  12:             if (this.detail != value)
  13:             {
  14:                 this.detail = value;
  15:                 this.RaisePropertyChanged("Detail");
  16:             }
  17:         }
  18:     } 
  19:  
  20:     #endregion
  21:  
  22:     #region Related property
  23:     
  24:     private RelatedNamesViewModel related;
  25:  
  26:     public RelatedNamesViewModel Related
  27:     {
  28:         get { return this.related; }
  29:         set
  30:         {
  31:             if (this.related != value)
  32:             {
  33:                 this.related = value;
  34:                 this.RaisePropertyChanged("Related");
  35:             }
  36:         }
  37:     } 
  38:  
  39:     #endregion
  40:  
  41:     public NameViewModel()
  42:     {
  43:         this.Detail = new NameDetailViewModel();
  44:         this.Related = new RelatedNamesViewModel();
  45:     }
  46:  
  47:     public void Load(string parameter)
  48:     {
  49:         int id = 0;
  50:  
  51:         bool success = int.TryParse(parameter, out id);
  52:  
  53:         if (success)
  54:         {
  55:             IDataService data = new DataService();
  56:             PersonName name = data.GetName(id);
  57:             this.Detail.Load(name);
  58:             this.Related.Load(name);
  59:         }
  60:     }
  61: }

This view model is the only that requires a short trick into the page codebehind. This is because it is not a ggod idea to load the ViewModel content into the ctor but you have to wait for the OnNavigated event where you get the id of the selected item coming from the navigation uri. So using a simple ILoadable interface I create a lightweight connection with the codebehind and you are able to create a base class for pages that implicitly does this operation:

   1: public sealed partial class NamePage : Page
   2: {
   3:     public NamePage()
   4:     {
   5:         this.InitializeComponent();
   6:     }
   7:  
   8:     protected override void OnNavigatedTo(NavigationEventArgs e)
   9:     {
  10:         ILoadable vm = this.DataContext as ILoadable;
  11:  
  12:         if (vm != null)
  13:             vm.Load((string)e.Parameter);
  14:     }
  15: }

The two user controls have an independent view model and act with a complete separation from the other. This enable to easily reuse this component and also to easily customize its graphical content and to unit test it. Here is the XAML from grid view User Control:

   1: <GridView ItemsSource="{Binding Names}" SelectionMode="None" IsItemClickEnabled="True">
   2:      <bh:Interaction.Behaviors>
   3:          <svc:EventToCommand Command="{Binding ItemClickedCommand}" EventName="ItemClick" UseEventArgs="True" />
   4:      </bh:Interaction.Behaviors>
   5:      <GridView.ItemsPanel>
   6:          <ItemsPanelTemplate>
   7:              <VariableSizedWrapGrid ItemWidth="200" MaximumRowsOrColumns="6" Orientation="Vertical" />
   8:          </ItemsPanelTemplate>
   9:      </GridView.ItemsPanel>
  10:      <GridView.ItemContainerStyle>
  11:          <Style TargetType="GridViewItem">
  12:              <Setter Property="HorizontalAlignment" Value="Stretch" />
  13:              <Setter Property="HorizontalContentAlignment" Value="Stretch" />
  14:          </Style>
  15:      </GridView.ItemContainerStyle>
  16:      <GridView.ItemTemplate>
  17:          <DataTemplate>
  18:              <Border BorderThickness="10,0,0,0" BorderBrush="{Binding Gender, Converter={StaticResource Bool2GenderColor}}" Margin="0,20,0,0" Padding="20,0,0,0" HorizontalAlignment="Left">
  19:                  <TextBlock Style="{StaticResource SubheaderTextStyle}" Text="{Binding Name}" HorizontalAlignment="Left" />
  20:              </Border>
  21:          </DataTemplate>
  22:      </GridView.ItemTemplate>
  23:  </GridView>

In this snippet you can see that the ViewModel exposes a "Names" property. It is connected via Binding to the ItemsSource property of the GridView. Each generated item is connected to the properties it has to display and a converter is used to map the boolean value representing the Gender to a color (Pink or Azure). The ViewModel is very simple in its structure. It does not contains any property that is strictly related with a "UI" concern. There is not a "NameColor" property but the conversion is made in the converter leaving the interpretation of the "Gender" information to the designer that is able to express it as a color or as a string on the basis of his personal feel. This ia an important concept to keep in mind while you design your ViewModel. It is only a agnostic representation of the information needed to the View but it does not have any "graphical" information. Properties like "IsVisible", "IsEnabled", etc are strongly discouraged but they must come from an interpretation of a particular condition of the ViewModel's data. The ViewModel is pretty simple:

   1: public class RelatedNamesViewModel: ViewModelBase
   2: {
   3:     public ObservableCollection<PersonName> Names { get; set; }
   4:  
   5:     public RelayCommand<ItemClickEventArgs> ItemClickedCommand { get; set; }
   6:  
   7:     public RelatedNamesViewModel()
   8:     {
   9:         this.Names = new ObservableCollection<PersonName>();
  10:         this.ItemClickedCommand = new RelayCommand<ItemClickEventArgs>(ItemClicked);
  11:     }
  12:  
  13:     private void ItemClicked(ItemClickEventArgs args)
  14:     {
  15:         this.MessengerInstance.Send<PersonName>((PersonName)args.ClickedItem, "NameSelected");
  16:     }
  17:  
  18:     public void Load(PersonName name)
  19:     {
  20:         DataService service = new DataService();
  21:         IEnumerable<PersonName> names = service.GetRelated(name.Id);
  22:         
  23:         this.Names.Clear();
  24:  
  25:         foreach(var item in names)
  26:             this.Names.Add(item);
  27:     }
  28: }

Each ViewModel has three specific sections: Initialization (in the constructor) where properties are initialized to a default value; Loading is the method that is responsible to call the Model and load the retrieved information to the properties. Finally the other section is related to the command handling. In this particular case the ViewModel handles the command coming from the ItemClick event. Here I use the RelayCommand class from the MVVM Light Toolkit.

The command is not handled directly inside the ViewModel because it has to be notified to the other part of the View. This is made sending a "broadcast message" with the Messenger (also part of the MVVM Light Toolkit). Broadcasting a message instead of handling all into a single ViewModel has a number of advantages. This let you, as an example, to add in the future other View (and ViewModels) that subscribe to the message and act adding other information, like "suggested variants", "other people choices", etc... You are not forced to put all this logic together in the same ViewModel but you can build the "bricks", one by one, when you really need them.

The messenger gets a name for the event. To give a name to this event is not a trivial task. When I name the event I prefer to choose an agnostic name (in the case "NameSelected") instead to something that say what it is expected to happen (e.g. "ShowItem"). This make it easy to interpret the message in different ways in the future.

The other ViewModel receives the message and update the item to be displayed simply calling again the Load method:

   1: public class NameDetailViewModel : ViewModelBase
   2: {
   3:     #region Item property
   4:  
   5:     private PersonName item;
   6:  
   7:     public PersonName Item
   8:     {
   9:         get { return this.item; }
  10:         set
  11:         {
  12:             if (this.item != value)
  13:             {
  14:                 this.item = value;
  15:                 this.RaisePropertyChanged("Item");
  16:             }
  17:         }
  18:     }
  19:  
  20:     #endregion
  21:  
  22:     public NameDetailViewModel()
  23:     {
  24:         this.MessengerInstance.Register<PersonName>(this, "NameSelected", Load);
  25:     }
  26:  
  27:     public void Load(PersonName item)
  28:     {
  29:         this.Item = item;
  30:     }
  31: }

The item property uses the INotifyPropertyChanged interface to make the View aware of the change. Also in this case I use the MVVM Light Toolkit to raise the event, inheriting the ViewModel from the ViewModelBase class. It exposes the RaisePropertyChanged method that does all the work.

Map Events to commands.

In Windows Store Apps, like in Silverlight, only Button and HyperlinkButton natively support the Command pattern. So to map the ItemClick event to the ViewModel you need to apply a trick. The most simple is to operate in the codebehind, but you can search Nuget for a library called "WinRtBehaviors". This library is an implementation of Blend Behaviors worte for WinRt and on the basis of its code you can create a simple behavior called "EventToCommand" to map an event to a command using only the markup and Binding extension.

   1: public class EventToCommand : Behavior<FrameworkElement>
   2: {
   3:     #region EventName
   4:  
   5:     public static readonly DependencyProperty EventNameProperty =
   6:         DependencyProperty.Register(
   7:             "EventName",
   8:             typeof(string),
   9:             typeof(EventToCommand),
  10:             new PropertyMetadata(null));
  11:  
  12:     public string EventName
  13:     {
  14:         get { return (string)GetValue(EventNameProperty); }
  15:         set { SetValue(EventNameProperty, value); }
  16:     }
  17:  
  18:     #endregion
  19:  
  20:     #region Command
  21:  
  22:     public static readonly DependencyProperty CommandProperty =
  23:         DependencyProperty.Register(
  24:             "Command",
  25:             typeof(ICommand),
  26:             typeof(EventToCommand),
  27:             new PropertyMetadata(null));
  28:  
  29:     public ICommand Command
  30:     {
  31:         get { return (ICommand)GetValue(CommandProperty); }
  32:         set { SetValue(CommandProperty, value); }
  33:     }
  34:  
  35:     #endregion
  36:  
  37:     #region CommandParameter
  38:  
  39:     public static readonly DependencyProperty CommandParameterProperty =
  40:         DependencyProperty.Register(
  41:             "CommandParameter",
  42:             typeof(object),
  43:             typeof(EventToCommand),
  44:             new PropertyMetadata(null));
  45:  
  46:     public object CommandParameter
  47:     {
  48:         get { return (object)GetValue(CommandParameterProperty); }
  49:         set { SetValue(CommandParameterProperty, value); }
  50:     }
  51:  
  52:     #endregion
  53:  
  54:     #region UseEventArgs
  55:  
  56:     public static readonly DependencyProperty UseEventArgsProperty =
  57:         DependencyProperty.Register(
  58:             "UseEventArgs",
  59:             typeof(bool),
  60:             typeof(EventToCommand),
  61:             new PropertyMetadata(null));
  62:  
  63:     public bool UseEventArgs
  64:     {
  65:         get { return (bool)GetValue(UseEventArgsProperty); }
  66:         set { SetValue(UseEventArgsProperty, value); }
  67:     }
  68:  
  69:     #endregion
  70:  
  71:     #region Handlers
  72:  
  73:     protected override void OnAttached()
  74:     {
  75:         if (this.AssociatedObject != null &&
  76:             !string.IsNullOrEmpty(this.EventName))
  77:         {
  78:             EventInfo eventInfo = this.GetEventInfo(this.AssociatedObject.GetType(), this.EventName);
  79:  
  80:             if (eventInfo != null)
  81:             {
  82:                 Delegate handler = this.GetEventHandler(eventInfo);
  83:  
  84:                 WindowsRuntimeMarshal.AddEventHandler<Delegate>(
  85:                     dlg => (EventRegistrationToken)eventInfo.AddMethod.Invoke(this.AssociatedObject, new object[] { dlg }),
  86:                     etr => eventInfo.RemoveMethod.Invoke(this.AssociatedObject, new object[] { etr }), handler);
  87:             }
  88:         }
  89:  
  90:         base.OnAttached();
  91:     }
  92:  
  93:     protected override void OnDetaching()
  94:     {
  95:         if (this.AssociatedObject != null &&
  96:             !string.IsNullOrEmpty(this.EventName))
  97:         {
  98:             EventInfo eventInfo = this.GetEventInfo(this.AssociatedObject.GetType(), this.EventName);
  99:  
 100:             if (eventInfo != null)
 101:             {
 102:                 Delegate handler = this.GetEventHandler(eventInfo);
 103:  
 104:                 WindowsRuntimeMarshal.RemoveEventHandler<Delegate>(
 105:                     etr => eventInfo.RemoveMethod.Invoke(this.AssociatedObject, new object[] { etr }), handler);
 106:             }
 107:         }
 108:  
 109:         base.OnDetaching();
 110:     }
 111:  
 112:     #endregion
 113:  
 114:     #region Event Management
 115:     
 116:     private EventInfo GetEventInfo(Type type, string eventName)
 117:     {
 118:         EventInfo eventInfo = type.GetTypeInfo().GetDeclaredEvent(eventName);
 119:  
 120:         if (eventInfo == null)
 121:         {
 122:             Type baseType = type.GetTypeInfo().BaseType;
 123:  
 124:             if (baseType != null)
 125:                 return GetEventInfo(type.GetTypeInfo().BaseType, eventName);
 126:             else
 127:                 return eventInfo;
 128:         }
 129:  
 130:         return eventInfo;
 131:     }
 132:  
 133:     /// <summary>
 134:     /// Create an instance of a delegate able to handle the provided event
 135:     /// </summary>
 136:     /// <param name="eventInfo">Information about the event to create the delegate</param>
 137:     /// <returns></returns>
 138:     public Delegate GetEventHandler(EventInfo eventInfo)
 139:     {
 140:         Delegate dlg = null;
 141:  
 142:         if (eventInfo == null)
 143:             throw new ArgumentNullException("eventInfo");
 144:  
 145:         if (eventInfo.EventHandlerType == null)
 146:             throw new ArgumentNullException("eventInfo.EventHandlerType");
 147:  
 148:         if (dlg == null)
 149:             dlg = this.GetType()
 150:                 .GetTypeInfo()
 151:                 .GetDeclaredMethod("OnEventRaised")
 152:                 .CreateDelegate(eventInfo.EventHandlerType, this);
 153:  
 154:         return dlg;
 155:     }
 156:  
 157:     /// <summary>
 158:     /// Received the notifications when the wrapped event has been raised.
 159:     /// </summary>
 160:     /// <param name="sender">source of the event</param>
 161:     /// <param name="e">arguments of the event</param>
 162:     private void OnEventRaised(object sender, object e)
 163:     {
 164:         if (this.Command != null)
 165:         {
 166:             if (this.UseEventArgs)
 167:                 this.Command.Execute(e);
 168:             else
 169:                 this.Command.Execute(this.CommandParameter);
 170:         }
 171:     } 
 172:  
 173:     #endregion
 174: }

The class is able to attach an event got from the EventName property and when the event is raised it call the ICommand instance attached by Binding to the Command property. The code in this snipped is slightly complex but its inner working is out of the topic of this article. You can easily use it as-is in your projects and you can attach an event with a code like this:

   1: <GridView ItemsSource="{Binding Names}" SelectionMode="None" IsItemClickEnabled="True">
   2:     <bh:Interaction.Behaviors>
   3:         <svc:EventToCommand Command="{Binding ItemClickedCommand}" EventName="ItemClick" UseEventArgs="True" />
   4:     </bh:Interaction.Behaviors>
   5:  
   6:     <!-- other code -->
   7:  
   8: </GridView>

You can download complete example attached to this article. I hope it can help you to give a better structure to your Windows Store projects and make them much more maintainable and testable.

Waiting for HTML

Starting from the attached sample you can create your own toolset, made of base classes, behaviors and converters, to better support MVVM in your Windows Store projects. The next (and last) article in this series will show how to implements MVVM with HTML in a windows Store app.


Subscribe

Comments

No comments

Add Comment

Login to comment:
  *      *       

From this series