Data Binding in Universal Windows Apps

2015-10-23 16:02:22来源:作者:Adrian Hall人点击

I’m currently writing a simple task list as a Universal Windows App as part of my study to onboard onto my new job in Microsoft Azure. In the last article I explorer responsive design techniques for universal apps. Today was all about data binding. Explicitly, I wanted to create a simple task list and show it off in a scrollable list view. I figured I was going to need some sort of task store, so I also wanted to implement a simple CRUD store for holding my tasks.

Let’s go to the code. My model is called TaskItem – I’m placing both the model and the store in their own namespace. That way I can put them into their own project down the road if I need to share models between the UWP and, say, a web site. The Models/TaskItem.cs is fairly simple:

namespace QuickStart.UWP.Models{class TaskItem{public string Id { get; set; }public string Title { get; set; }public bool Completed { get; set; }}}

I know that my backend is likely to need a string as the Id, although I could choose anything and get the backend to convert it. Then I have the two fields I actually want to show off. These could be anything. I’m creating a task list, so I have the title and the completed flag. To go along with this, I have a simple store in Models/TaskStore.cs :

using System;using System.Collections.Generic;using System.Threading.Tasks;// This file includes non-await code for an async interface// it will execute synchronously - we are ok with that right// now as the whole system will be replaced with an async// version later on#pragma warning disable 1998namespace QuickStart.UWP.Models{/// <summary>/// Implementation of the TaskStore - this is a basic/// CRUD type store./// </summary>class TaskStore{private List<TaskItem> _items;public TaskStore(){_items = new List<TaskItem>();_items.Add(new TaskItem { Id = "0", Title = "Task 1" });_items.Add(new TaskItem { Id = "1", Title = "Task 2" });}public async Task CreateTaskItem(TaskItem item){item.Id = Guid.NewGuid().ToString();_items.Add(item);}public async Task<TaskItem> RetrieveTaskItem(string itemID){return _items.Find(t => t.Id.Equals(itemID));}public async Task UpdateTaskItem(TaskItem item){var idx = _items.FindIndex(t => t.Id.Equals(item.Id));if (idx >= 0){_items[idx] = item;}}public async Task DeleteTaskItem(TaskItem item){var idx = _items.FindIndex(t => t.Id.Equals(item.Id));if (idx >= 0){_items.RemoveAt(idx);}}public async Task<ICollection<TaskItem>> RetrieveTaskItems(){return _items;}}}

This is a fairly simple CRUD implementation. In fact, I’ve named the methods after the CRUD operations. Note line 9. My current implementation is synchronous, but I want it to ACT like an async store. The pragma will squelch the warning about using async in synchronous methods. When I implement a backend store (for example, SQLite or Azure App Service Mobile Apps), then this will become async and I will remove the pragma.

Now to the main feature – the XAML code. From my prior code, I’ve got a MainPage.xaml with a scroll viewer in it. Within that, I’m going to implement a list view that does the 2-way data binding:

<ScrollViewer Grid.Row="2" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Auto"><ListView x:Name="ListItems" Margin="5,5,0,0"><ListView.ItemTemplate><DataTemplate><StackPanel Orientation="Horizontal"><CheckBox Name="CheckBoxComplete" IsChecked="{Binding Completed, Mode=TwoWay}" Margin="10,5" VerticalAlignment="Center" Content="{Binding Title}"/></StackPanel></DataTemplate></ListView.ItemTemplate></ListView></ScrollViewer>

The ScrollViewer puts scroll bars on whatever is inside it. I’ve hidden the horizontal scroll bar and made the vertical scroll bar pop up when the need arises. The contents is the ListView – a control that just repeats template across a bound entity. I’ve named this – more on why later. The template is a data template. The main thing to note here is the {Binding} elements. You can make the binding one way (the default) or two way. Each one has a property name and that is matched on the bound entity. My entity is going to be a list of TaskItems – each TaskItem has a title and a completed flag.

This doesn’t do anything without having something to iterate over. On the ListView, that’s the ItemsSource property, which takes – well – a list. You could have probably guessed that.

To set up the ItemsSource, I need to load it from the TaskStore . This is done in the MainPage.xaml.cs backing code file. First up, I have to create a TaskStore:

private TaskStore store; public MainPage() { store = new TaskStore(); this.InitializeComponent(); SizeChanged += MainPage_SizeChanged; }

Since UWP applications only have one Main Page, this is safe. If it was less safe, I would ensure that the TaskStore is a singleton pattern and use TaskStore.Instance() instead. Next, I need to have a method that refreshes the items in the ItemsList control:

/// <summary>/// Refresh the items in the list./// </summary>private async void RefreshItems(){try{ListItems.ItemsSource = (List<TaskItem>)(await store.RetrieveTaskItems());}catch (Exception ex){await new MessageDialog(ex.Message, "Error loading tasks").ShowAsync();}}

I’ve defined our TaskStore with an async interface with the expectation that it will be async in the future. That means I need to await for the results.

Finally, when the Main Page is brought up, I want to refresh the items. There is a special navigation event handler that I can use to do this:

/// <summary> /// Refresh the contents of the list when the page is loaded /// </summary> /// <param name="e"></param> protected override async void OnNavigatedTo(NavigationEventArgs e) { RefreshItems(); }

I don’t need to wire this up – this is a special event handler that is called no matter what happens. It’s defined by default in the Page object, so you have to override it to use it.

One of the things to note is that this is a two-way binding. That means the model is updated when the UI updates it. For instance, I created a CheckBoxComplete_Checked event handler to handle the case when a checkbox was checked. I put a debug line in there and set a breakpoint on the debug line so I could inspect the state. Taking a look at the store that I created in the main page, I saw this:

Note that the Completed flag is true. This reflects the state of the checkbox. This can be very useful when doing automatic saves, background loading or filtering. If you change the list, your UI updates automatically. However, I want to moderate all access through my store. This will make saving and such like easier later. As a result, I dropped the Mode=Twoway from the XAML declaration. Now I want to call store.UpdateTaskItem() in my event handler:

/// <summary>/// Event handler that is called when a checkbox is set or cleared/// </summary>/// <param name="sender"></param>/// <param name="e"></param>private async void CheckBoxComplete_Checked(object sender, RoutedEventArgs e){CheckBox checkbox = (CheckBox)sender;TaskItem item = checkbox.DataContext as TaskItem;item.Completed = (bool)checkbox.IsChecked;await store.UpdateTaskItem(item);await RefreshItems();}

My XAML becomes this:

<CheckBox Name="CheckBoxComplete" IsChecked="{Binding Completed}" Margin="10,5" VerticalAlignment="Center" Content="{Binding Title}" Checked="CheckBoxComplete_Checked" Unchecked="CheckBoxComplete_Checked"/>

Now I have the one-way binding that I want. When I click on a checkbox, the event handler changes the value in the TaskItem and updates the store. It then refreshes the data binding from the store, which will update the task list.

Talking of filtering, that’s my next task, and I’ll tackle that in my next blog post. In the mean time, keep up with the sameple code I am writing by checkout out my GitHub repository .