Why Declarative UIs Are Important


Url: http://wilderminds.blob.core.windows.net/downloads/declarativeui.zip

Sketching a UIIf you're building Internet applications, you've probably been bombarded with lots of new technologies the last few years: Silverlight, Flex, ASP.NET MVC, jQuery, etc. While all of these technologies have their use-cases, many of them are pointing to something that is new and important.

The central idea of separating concerns is not new. Back in SmallTalk when Model-View-Controller was born, the benefits of isolating layers of an application were pretty apparent. The problem was that it wasn't easy to implement. The problem with implementation was never the code...or was exactly the code. The problem is that when you are working on a problem in the user interface, it is far to easy to try and solve it directly in the code. Its not laziness or lack of talent.  Its simply a matter of focus sometimes. Under the timeline gun its easy to be myopic about a problem. Its simply too easy to just try and solve a problem by writing code in the user interface (or the View).

This is why I think that Declarative User Interfaces are so important. But what are Declarative User Interfaces? Simply put, Declarative User Interfaces (or Declarative UIs) are defining views using markup to deliver content and behavior. The two most obvious examples in the Microsoft space are XAML and MVC Views. What makes Declarative UI's so seductive is that fact that putting non-UI code (or business logic) in the markup is not only difficult but tends to feel wrong. That sort of code sticks out like a thumb. In my opinion, it encourages the behavior of keeping the View as purely user interface code. Let's use Silverlight as an example of building using a Declarative UI.

An Example

I started by staying completely in Blend to do my design. My basic, but simple idea, was an editor for XBox games (yeah, I know) that could select by Genre the games to view. Here's my original concept:

Blend Concept #1

Now that idea is fine, but I needed some data to clarify my ideas so I used the Blend Sample Data feature to create some dummy data. At this point all I am doing is proving my design, but at the same time I am getting a data shape that development (me in this case, but possibly different people) will use later as the data I want to edit.  By defining this shape, I am allowing myself to do the data binding. My Sample Data in Blend looks something like this:

Blend Sample Data

Once data bound, my initial user interface looks like so:

Data Bound in Blend

Normally, this would be the time that the user interface would initially be handed over to development. Since I wrote everything, I moved on to Visual Studio. The benefit of doing this much work in Blend first is that all I need to do is handle creating the data and replace the sample data with my own data and we can move on.

Once in Visual Studio, you should notice that the sample data is now part of your project.  The Sample data is included in the App.XAML file like so:

<Application xmlns="..."
             xmlns:x="..."
             xmlns:SampleData="..."
             xmlns:d="..."
             xmlns:mc="..."
             x:Class="DeclarativeUI.App"
             mc:Ignorable="d">
  <Application.Resources>
    <!-- Resources scoped at the Application 
         level should be defined here. -->
    <SampleData:GamesData x:Key="GamesData"
                          d:IsDataSource="True" />
  </Application.Resources>
</Application>

This data is used in the main view simply as its DataContext (because that's how I bound the sample data in Blend):

<UserControl xmlns="..."
             xmlns:x="..."
             x:Class="DeclarativeUI.MainPage"
             DataContext="{Binding Source={StaticResource GamesData}}">

Now that I am in Visual Studio, I can start replacing the Sample Data. I built a Model and a ViewModel (see my MSDN Magazine article here for more information on MVVM in Silverlight). The ViewModel simply exposed the same sort of data that the sample data does (though I am returning a fuller Game and Genre object since there is more data in the database):

public interface IMainViewModel : IViewModel
{
  ObservableCollection<Genre> Genres { get; }
  ObservableCollection<Game> Games { get; }

  void GetGenresAsync();
  void GetGamesByGenreAsync(object sender, EventArgs args);
  void SaveAllAsync();
}

The ViewModel looks like the Sample Data so simply setting the DataContext with my ViewModel should work at showing the sample data. But we're only partly there. Note that the ViewModel interface exposes both the Genres and Games collections that we defined in the sample data, but also exposes the methods that performs the data access verbs. This is where Declarative UIs can be really powerful.

In Silverlight 3, the Blend team introduced Behaviors to simplify actions and triggers in XAML. For the WPF-aware readers, Behaviors are different in that they are pre-packaged bundles of functionality. While most behaviors are tied to performing actions on the user interface directly (e.g. ControlStoryboardAction) they are not technologically limited in that way. In my early experiments with Behaviors, I had created a simple behavior to call into a ViewModel. Because Behaviors can work with data binding, it becomes an easy way to interact with the ViewModel (without having a hard connection to it). They allow our UI's to be declarative but still separating those concerns.

My Behavior had a lot of problems with it and I had been looking for time to revisit it when I ran into the Blend team's new sample behaviors:

http://expressionblend.codeplex.com

This project adds a number of behaviors to Blend including several specifically for data-driven applications. While all the data-driven behaviors are interesting, i'll focus on one for our problem: CallDataMethod. This behavior allows you to call a method on an object (usually the DataContext) when an event fires. 

In our XAML code, I decided that I needed to load the combobox full of Genres as the first data call. Since I am allowing users to filter by Genre, the combobox will be the source of that filtering.  So I added the Behavior which looks like this in XAML:

<i:Interaction.Triggers>
  <i:EventTrigger EventName="Loaded">
    <si:CallDataMethod Method="GetGenresAsync" />
  </i:EventTrigger>
</i:Interaction.Triggers>

The behavior simply reacts to the UserControl's Loaded event (so that this fires when the XAML is loaded) and calls the GetGenresAsync method. If you look at our interface, you will notice that GetGenresAsync method takes no parameters which is one of the signatures that the CallDataMethod behaviors supports.

When the Loaded event fires, the behavior calls GetGenresAsync which starts the call to the server to get the data.  When that operation comples, the ViewModel simply fills in the Genres into the Genres collection on the ViewModel so that it is shown in UI.  We could throw an event to the UI, but that's not necessary because the same return can show the error information. How?  Let's look at the ViewModel code:

void theModel_OnGenresCompleted(object sender, EntityEventArgs<Genre> e)
{
  if (e.Error != null)
  {
    ErrorMessage = e.Error.ToString();
  }
  else
  {
    theGenres.Clear();

    foreach (var g in e.Results.OrderBy(g => g.Name))
    {
      theGenres.Add(g);
    }
  }

  IsBusy = false;
}

Notice when the Genres are returned from the server that we are sending in whether there was an error.  And if an error was returned, we simply set the string on an ErrorMessage property. This is a property that the UI is data bound to in order to show the error.  This way we still don't need an event thrown to the UI to communicate the error. This is a pattern in ViewModels I use quite a lot as it helps our UI's stay declarative and clean.

To load the DataGrid with games, I needed to use the CallDataMethod trigger again. But in this case, I need to be able to tell the ViewModel which Genre was selected. The CallDataMethod takes a very simple approach and simply will allow you to use the standard event syntax as well (see how the interface supports the standard event signature of an object and an EventArgs):

<ComboBox x:Name="Genres"
          HorizontalAlignment="Left"
          Margin="8,2,0,0"
          Width="150"
          Grid.Row="1"
          DisplayMemberPath="Name"
          ItemsSource="{Binding Genres, Mode=OneWay}">
  <i:Interaction.Triggers>
    <i:EventTrigger EventName="SelectionChanged">
      <si:CallDataMethod Method="GetGamesByGenreAsync" />
    </i:EventTrigger>
  </i:Interaction.Triggers>
</ComboBox>

So when the SelectionChanged event is fired on the combobox, we call the GetGamesByGenreAsync method.  That method has the standard event syntax so that in the event we can retrieve the selected Genre:

public void GetGamesByGenreAsync(object sender, EventArgs e)
{
  IsBusy = true;
  var sel = sender as Selector;
  if (sel != null && sel.SelectedItem is Genre)
  {
    theModel.GetGamesByGenreAsync(((Genre)sel.SelectedItem).Name);
  }
}

Note that this method assumes that the UI is using some sort of Selector (from which ListBox, ComboBox and ItemsControl all derive) which is a concession to the separation. But if it weren't a Selector, we could probably craft this same experience with other behaviors. Because we can get the selected element from the Selector, our View still is very declarative.

This is why I think declarative UIs are so powerful. We are leaving all the look and feel of the application directly in the markup. So that the XAML is self describing and it makes it harder to write business logic code in the user interface.  That should be left in the other tiers where it belongs.

So Declarative User Interfaces are Code-less?

The answer is yes and no. While declarative user interfaces can be codeless in many cases, it should not be dogmatic. If it makes sense to write code in a view, write the code.  The power here is to do most of the work required in the markup itself. Purity for purity sake is not helpful. But if you do write code in your view, it better not be business logic.  If you need business logic, communicate back to the VM or with the data objects themselves. That's where it belongs.

You can get the source to this sample here (which includes both an "AfterBlend" version and a "AfterVS" version):

http://wilderminds.blob.core.windows.net/downloads/declarativeui.zip

What do you think?



Shawn
Shawn Wildermuth
Author, Teacher, and Coach




My Courses

Wilder Minds Training
Vue.js by Example
Bootstrap 4 by Example
Intro to Font Awesome 5 (Free Course)
Pluralsight
Designing RESTful Web APIs (new)
Building an API with ASP.NET Web API
Building an API with ASP.NET Core
Building a Web App with ASP.NET Core, MVC6, EF Core, Bootstrap and Angular
Less: Getting Started

Application Name WilderBlog Environment Name Production
Application Ver v4.0.30319 Runtime Framework x86
App Path D:\home\site\wwwroot\ Runtime Version .NET Core 3.0.0
Operating System Microsoft Windows 10.0.14393 Runtime Arch X86