☕ Wired Brain Coffee
Basic layout
MainPage.xaml
<Window
x:Class="WiredBrainCoffee.UWP.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:WiredBrainCoffee.UWP"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="350"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Border Grid.ColumnSpan="2" Background="#f05a28">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Image Height="90" Margin="5" Source="/Images/logo.png"/>
<TextBlock Text="Employee Manager" FontSize="40" VerticalAlignment="Center"/>
</StackPanel>
</Border>
<!-- Sidebar -->
<Grid Grid.Row="1">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition />
</Grid.RowDefinitions>
<Button Content="Refresh" Margin="10"/>
<ListView Grid.Row="1"/>
</Grid>
<!--MainArea-->
<Grid Grid.Row="1" Grid.Column="1">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBox Header="Firstname" Margin="10"/>
<DatePicker Grid.Row="1" Header="Entry date" Margin="10"/>
<ComboBox Grid.Row="2" Header="Job role" Margin="10" HorizontalAlignment="Stretch"/>
<CheckBox Grid.Row="3" Content="Is coffee drinker?" Margin="10"/>
<Button Grid.Row="4" Content="Save" Margin="10 10 10 30"
VerticalAlignment="Bottom" HorizontalAlignment="Left"/>
</Grid>
</Grid>
</Window>
Custom control
Controls/HeaderControl.xaml
<UserControl
x:Class="WiredBrainCoffee.UWP.Controls.HeaderControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:WiredBrainCoffee.UWP.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="300"
d:DesignWidth="400">
<Border Background="#F05A28">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Image Source="/Images/WiredBrainLogo.png" Height="90"/>
<TextBlock Text="Wired Brain Coffee" FontSize="40" VerticalAlignment="Center"/>
</StackPanel>
<Button HorizontalAlignment="Right" Grid.Column="1" Margin="10">
<SymbolIcon Symbol="AlignRight"/>
</Button>
</Grid>
</Border>
</UserControl>
MainPage.xaml
<Page
x:Class="WiredBrainCoffee.UWP.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:WiredBrainCoffee.UWP"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="using:WiredBrainCoffee.UWP.Controls"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="350"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<!-- Header -->
<controls:HeaderControl Grid.ColumnSpan="2"/>
<!-- Sidebar -->
<Grid Grid.Row="1">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal">
<Button Margin="10">
<SymbolIcon Symbol="AddFriend"/>
</Button>
<Button Margin="10">
<SymbolIcon Symbol="Delete"/>
</Button>
</StackPanel>
<ListView Grid.Row="1">
<ListViewItem>Aristotle</ListViewItem>
<ListViewItem>Euclid</ListViewItem>
<ListViewItem>Plato</ListViewItem>
<ListViewItem>Socrates</ListViewItem>
</ListView>
</Grid>
<StackPanel Grid.Row="1" Grid.Column="1">
<TextBox Header="First name" Margin="10"/>
<TextBox Header="Last name" Margin="10"/>
<CheckBox Content="Drinks coffee" Margin="10"/>
</StackPanel>
</Grid>
</Page>
Sidebar
Setting an x:Name
attribute on an element allows it to be manipulated in C#. (src)
MainPage.xaml.cs
private void btn_MoveSideBar_Click(object sender, RoutedEventArgs e)
{
int column = Grid.GetColumn(customerListGrid);
int newcolumn;
if (column == 0)
{
newcolumn = 2;
btn_MoveSideBar_Symbol.Symbol = Symbol.AlignLeft;
}
else
{
newcolumn = 0;
btn_MoveSideBar_Symbol.Symbol = Symbol.AlignRight;
}
Grid.SetColumn(customerListGrid, newcolumn);
}
Data provider
A data provider class accomodates the need for mock data while also loosely coupling the data with the source. (src)
DataProviders/CustomerDataProvider.cs
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.IO;
using Windows.Storage;
using Windows.Storage.Streams;
using WiredBrainCoffee.UWP.Models;
namespace WiredBrainCoffee.UWP.DataProviders
{
class CustomerDataProvider
{
private static readonly string _customersFileName = "customers.json";
private static readonly StorageFolder _localFolder = ApplicationData.Current.LocalFolder;
public async Task<IEnumerable<Customer>> LoadCustomersAsync()
{
var storageFile = await _localFolder.TryGetItemAsync(_customersFileName) as StorageFile;
List<Customer> customerList = null;
if (storageFile == null)
{
customerList = new List<Customer>
{
new Customer{FirstName="Clark",LastName="Kent",IsCoffeeDrinker=true},
new Customer{FirstName="Bruce",LastName="Wayne",IsCoffeeDrinker=false},
new Customer{FirstName="Diana",LastName="Prince",IsCoffeeDrinker=true}
};
}
else
{
using (var stream = await storageFile.OpenAsync(FileAccessMode.Read))
{
using (var dataReader = new DataReader(stream))
{
await dataReader.LoadAsync((uint)stream.Size);
var json = dataReader.ReadString((uint)stream.Size);
customerList = JsonConvert.DeserializeObject<List<Customer>>(json);
}
}
}
return customerList;
}
public async Task SaveCustomersAsync(IEnumerable<Customer> customers)
{
var storageFile = await _localFolder.CreateFileAsync(_customersFileName, CreationCollisionOption.ReplaceExisting);
using (var stream = await storageFile.OpenAsync(FileAccessMode.ReadWrite))
{
using (var dataWriter = new DataWriter(stream))
{
var json = JsonConvert.SerializeObject(customers, Formatting.Indented);
dataWriter.WriteString(json);
await dataWriter.StoreAsync();
}
}
}
}
}
Models/Customer.cs
namespace WiredBrainCoffee.UWP.Models
{
public class Customer
{
public string FirstName { get; set; }
public string LastName { get; set; }
public bool IsCoffeeDrinker { get; set; }
}
}
Event hooks are used to populate the ListView
with data from the data provider.
MainPage.xaml.cs
public MainPage()
{
this.InitializeComponent();
this.Loaded += MainPage_LoadedAsync;
App.Current.Suspending += App_SuspendingAsync;
_customerDataProvider = new CustomerDataProvider();
}
private async void App_SuspendingAsync(object sender, Windows.ApplicationModel.SuspendingEventArgs e)
{
var deferral = e.SuspendingOperation.GetDeferral();
await _customerDataProvider.SaveCustomersAsync(customerListView.Items.OfType<Customer>());
deferral.Complete();
}
private async void MainPage_LoadedAsync(object sender, RoutedEventArgs e)
{
customerListView.Items.Clear();
var customers = await _customerDataProvider.LoadCustomersAsync();
foreach (var customer in customers)
{
customerListView.Items.Add(customer);
}
}
Data binding using events
Synchronize the customer detail textboxes to the selected item in the ListView. A rough form of data binding is possible with event handling. (src)
First implement an event handler when the ListView.SelectionChanged
event is fired.
MainPage.xaml
<ListView
Grid.Row="1"
x:Name="customerListView"
DisplayMemberPath="FirstName"
SelectionChanged="customerListView_SelectionChanged"/>
MainPage.xaml.cs
private void customerListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var customer = customerListView.SelectedItem as Customer;
txtFirstName.Text = customer?.FirstName ?? "";
txtLastName.Text = customer?.LastName ?? "";
chkDrinksCoffee.IsChecked = customer?.IsCoffeeDrinker;
}
Implement event handlers on the controls in the main area (TextBox.TextChanged
and CheckBox.Checked
and CheckBox.Unchedked
events) when changes are made.
MainPage.xaml
<StackPanel
Grid.Row="1"
Grid.Column="1">
<TextBox
x:Name="txtFirstName"
Header="First name"
Margin="10"
TextChanged="UpdateCustomer"/>
<TextBox
x:Name="txtLastName"
Header="Last name"
Margin="10"
TextChanged="UpdateCustomer"/>
<CheckBox
x:Name="chkDrinksCoffee"
Content="Caffeine fiend"
Margin="10"
Checked="UpdateCustomer"
Unchecked="UpdateCustomer"/>
</StackPanel>
MainPage.xaml.cs
private void UpdateCustomer(object sender, RoutedEventArgs e)
{
var customer = customerListView.SelectedItem as Customer;
if (customer != null)
{
customer.FirstName = txtFirstName.Text;
customer.LastName = txtLastName.Text;
customer.IsCoffeeDrinker = chkDrinksCoffee.IsChecked.GetValueOrDefault();
}
}
Update ListView
ListView
still won't update as a result of changes.
In order to implement this, you have to raise the PropertyChanged
event.
We implement the INotifyPropertyChanged
interface and make it the base class of Customer.
Also, we implement a helper method to fire the event handler whenever a property is changed.
This helper is invoked every time a property is set.
The CallerMemberName
attribute passes the name of the calling property as a string, and allows us to avoid placing typeof(FirstName)
, etc with every invocation. (src)
Models/Customer.cs
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace WiredBrainCoffee.UWP.Models
{
public class Observable : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class Customer : Observable
{
private string firstName;
private string lastName;
private bool isCoffeeDrinker;
public string FirstName
{
get => firstName;
set
{
firstName = value;
OnPropertyChanged();
}
}
public string LastName
{
get => lastName;
set
{
lastName = value;
OnPropertyChanged();
}
}
public bool IsCoffeeDrinker
{
get => isCoffeeDrinker;
set
{
isCoffeeDrinker = value;
OnPropertyChanged();
}
}
}
}
Add/remove customers
Implement event handlers for the Add and Delete buttons. (src)
MainPage.xaml.cs
private void DeleteCustomer_Click(object sender, RoutedEventArgs e)
{
var customer = customerListView.SelectedItem;
if (customer != null)
{
customerListView.Items.Remove(customer);
}
}
private void AddCustomer_Click(object sender, RoutedEventArgs e)
{
var customer = new Customer { FirstName = "New" };
customerListView.Items.Add(customer);
customerListView.SelectedItem = customer;
}
MainPage.xaml
<Button x:Name="AddCustomer" Margin="10"
Click="AddCustomer_Click" >
<SymbolIcon Symbol="AddFriend"/>
</Button>
<Button x:Name="DeleteCustomer" Margin="10"
Click="DeleteCustomer_Click">
<SymbolIcon Symbol="Delete"/>
</Button>
Custom control
We abstract controls in the main area of the app into a new CustomerDetailControl. As before, we cut the UI elements into a new XAML file and reference the new control in MainPage. However, now, customerListView is inaccessible.
The lynchpin is forming a property on customerDetailControl that is populated with the customer object by the SelectionChanged
event handler
MainPage.xaml.cs
private void customerListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var customer = customerListView.SelectedItem as Customer;
customerDetailControl.Customer = customer;
}
Controls/CustomerDetailControl.xaml
<UserControl
x:Class="WiredBrainCoffee.UWP.Controls.CustomerDetailControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:WiredBrainCoffee.UWP.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="300"
d:DesignWidth="400">
<StackPanel>
<TextBox
x:Name="txtFirstName"
Header="First name"
Margin="10"
TextChanged="UpdateCustomer"/>
<TextBox
x:Name="txtLastName"
Header="Last name"
Margin="10"
TextChanged="UpdateCustomer"/>
<CheckBox
x:Name="chkDrinksCoffee"
Content="Caffeine fiend"
Margin="10"
Checked="UpdateCustomer"
Unchecked="UpdateCustomer"/>
</StackPanel>
</UserControl>
Controls/CustomerDetailControl.xaml.cs
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using WiredBrainCoffee.UWP.Models;
namespace WiredBrainCoffee.UWP.Controls
{
public sealed partial class CustomerDetailControl : UserControl
{
public CustomerDetailControl()
{
this.InitializeComponent();
}
private Customer _customer;
public Customer Customer
{
get { return _customer; }
set
{
_customer = value;
txtFirstName.Text = _customer?.FirstName ?? "";
txtLastName.Text = _customer?.LastName ?? "";
chkDrinksCoffee.IsChecked = _customer?.IsCoffeeDrinker;
}
}
private void UpdateCustomer(object sender, RoutedEventArgs e)
{
if (Customer != null)
{
Customer.FirstName = txtFirstName.Text;
Customer.LastName = txtLastName.Text;
Customer.IsCoffeeDrinker = chkDrinksCoffee.IsChecked.GetValueOrDefault();
}
}
}
}
Assign mock content
We combine two different namespace mappings (one for the Customer model and another for the CustomerDetailControl) to prepopulate the CustomerDetailControl with a customer defined in XAML.
Because CustomerDetailControl exposes a public Customer property, this data can be assigned to the Customer
property using property-element syntax. (src)
MainPage.xaml
<controls:CustomerDetailControl x:Name="customerDetailControl" Grid.Row="1" Grid.Column="1">
<controls:CustomerDetailControl.Customer>
<model:Customer FirstName="Clark" LastName="Kent" IsCoffeeDrinker="True"/>
</controls:CustomerDetailControl.Customer>
</controls:CustomerDetailControl>
In order to be able to assign the customer as direct content without specifying the property explicitly, the custom control class has to be decorated with the ContentProperty
attribute.
This is because by default any direct child is assigned to the Content
property, which does not exist for this custom control.
Using the ContentProperty
allows us to specify a property to which to assign direct children.
Controls/CustomerDetailControl.xaml.cs
[ContentProperty(Name = nameof(Customer))]
public sealed partial class CustomerDetailControl : UserControl
{
/* ... */
}
MainPage.xaml
<controls:CustomerDetailControl x:Name="customerDetailControl" Grid.Row="1" Grid.Column="1">
<model:Customer FirstName="Clark" LastName="Kent" IsCoffeeDrinker="True"/>
</controls:CustomerDetailControl>
XAML Type conversion
Passing the customer as an attribute requires custom logic to parse the string.
The target model is then decorated with the CreateFromString
attribute.
This is only for custom classes: primitive types and enumerations can be parsed by the XAML processor automatically.
Models/CustomerConverter.cs
namespace WiredBrainCoffee.UWP.Models
{
public static class CustomerConverter
{
public static Customer ParseStringAsCustomer(string s)
{
string[] values = s.Split(';');
return new Customer { FirstName = values[0], LastName = values[1], IsCoffeeDrinker = bool.Parse(values[2]) };
}
}
}
Models/Customer.cs
[CreateFromString(MethodName ="WiredBrainCoffee.UWP.Models.CustomerConverter.ParseCustomerFromString")]
public class Customer : Observable
{
/* ... */
}
StaticResource
You can use the StaticResource Markup Extension to define the equivalent of XAML variables to store elements for attribution using attribute syntax.
Every UI element has a property named Resources
to which you can assign elements.
Unlike the Items
property of a ListView, however, this property is a Dictionary type, which means you must specify a key for these values (i.e. specify x:Key
).
Because the XAML processor looks for resources as it crawls up the element tree, these resources can be organized at any level of the application, even in the App.xaml where it will become available to other files: (src)
MainPage.xaml
<Page.Resources>
<model:Customer x:Key="Shazam" FirstName="William" LastName="William Batson" IsCoffeeDrinker="false"/>
</Page.Resources>
<!-- ... -->
<controls:CustomerDetailControl Customer="{StaticResource Shazam}"/>
However, mocking data in XAML is an anti-pattern; Resource dictionaries are typically used for colors and predefined strings.
Resource dictionaries are consolidated into their own files: (src)
Resources/Brushes.xaml
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<SolidColorBrush x:Key="customerListBackgroundBrush" Color="#EEEEEE"/>
</ResourceDictionary>
Resources/Strings.xaml
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<x:String x:Key="applicationTitle">Wired Brain Coffee</x:String>
</ResourceDictionary>
These can then be referenced from App.xaml and are available for assignment in any appropriate attribute
App.xaml
<Application
x:Class="WiredBrainCoffee.UWP.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:WiredBrainCoffee.UWP">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/Brushes.xaml"/>
<ResourceDictionary Source="Resources/Strings.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
Controls/HeaderControl.xaml
<UserControl
x:Class="WiredBrainCoffee.UWP.Controls.HeaderControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:WiredBrainCoffee.UWP.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="300"
d:DesignWidth="400">
<Border Background="#F05A28">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Image Source="/Images/WiredBrainLogo.png" Height="90"/>
<TextBlock Text="{StaticResource applicationTitle}" FontSize="40" VerticalAlignment="Center"/>
</StackPanel>
<Button x:Name="ButtonMove" HorizontalAlignment="Right" Grid.Column="1" Margin="10" Click="ButtonMove_Click">
<SymbolIcon x:Name="ButtonMove_Symbol" Symbol="AlignRight"/>
</Button>
</Grid>
</Border>
</UserControl>
ThemeResource
The ThemeResource Markup Extension makes UWP-specific theme resource dictionaries available. These same resources are available using StaticResource, but with ThemeResource they will be updated if the user changes his Windows theme from light to dark. (src)
Resources/Brushes.xaml
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Dark">
<SolidColorBrush x:Key="customerListBackgroundBrush" Color="#222222"/>
</ResourceDictionary>
<ResourceDictionary x:Key="Light">
<SolidColorBrush x:Key="customListBackgroundBrush" Color="#EEEEEE"/>
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>
Theme selection
A specific theme can be specified at any element by specifying a RequestedTheme
attribute.
However, this property cannot be changed at runtime.
App.xaml
<Application
x:Class="WiredBrainCoffee.UWP.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:WiredBrainCoffee.UWP"
RequestedTheme="Dark">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/Brushes.xaml"/>
<ResourceDictionary Source="Resources/Strings.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
A button to manually change theme involves simply assigning an ElementTheme
enum value to the MainPage's RequestedTheme
property.
However, because on startup this has an ApplicationTheme
enum value that is not evailable in ElementTheme
, the MainPage's constructor must be changed to set the correct theme enum.
Without this change, the first click after the application's startup will not change the theme at all, only set the correct ElementTheme
.
(src)
MainPage.xaml
<!-- Header -->
<controls:HeaderControl Grid.ColumnSpan="3"/>
<Button Grid.ColumnSpan="3" Click="ChangeTheme" Margin="10" VerticalAlignment="Top" HorizontalAlignment="Right">
<SymbolIcon Symbol="Placeholder"/>
</Button>
MainPage.xaml.cs
public MainPage()
{
this.InitializeComponent();
this.Loaded += MainPage_LoadedAsync;
App.Current.Suspending += App_SuspendingAsync;
_customerDataProvider = new CustomerDataProvider();
RequestedTheme = App.Current.RequestedTheme == ApplicationTheme.Dark
? ElementTheme.Dark
: ElementTheme.Light;
}
private void ChangeTheme(object sender, RoutedEventArgs e)
{
this.RequestedTheme = RequestedTheme == ElementTheme.Dark ? ElementTheme.Light : ElementTheme.Dark;
}
Color theme
The Fluent XAML Theme Editor on the Microsoft Store can generate ThemeResource dictionaries
Data binding
Use the Binding markup extension to establish a binding on the CustomerDetailControl to the Customer property of customerListView
Here, the Customer property is the target property, and the SelectedItem property of customerListView is the source property. So this data binding makes the information in the customerDetailControl (target) dependent on which item is selected (source).
MainPage.xaml.cs
<controls:CustomerDetailControl
x:Name="customerDetailControl" Grid.Row="1" Grid.Column="1"
Customer="{Binding ElementName=customerListView,Path=SelectedItem,Mode=OneWay}">
However, the target of a data binding needs to be a Dependency Property.
The purpose of dependency properties is to provide a way to compute the value of a property based on the value of other inputs.
The Visual Studio snippet for a dependency property is propdp
.
A dependency property includes a static readonly field of type DependencyProperty
and a normal property that works as a frontend for that field by wrapping GetValue
and SetValue
.
We implement the logic to update the controls with the selected customer as a callback function passed as the second argument of the PropertyMetadata
object in the dependency property definition.
This callback must be a static void function, and as such it has no access to the instantiated objects we have already named with x:Name
.
However, these objects are retrievable from the DependencyObject
and DependencyPropertyChangedEventArgs
parameters that are passed to the callback. (src)
Controls/CustomerDetailControl.xaml.cs
public Customer Customer
{
get { return (Customer)GetValue(CustomerProperty); }
set { SetValue(CustomerProperty, value); }
}
// Using a DependencyProperty as the backing store for Customer. This enables animation, styling, binding, etc...
public static readonly DependencyProperty CustomerProperty =
DependencyProperty.Register("Customer", typeof(Customer), typeof(CustomerDetailControl), new PropertyMetadata(null, CustomerChangedCallback));
private static void CustomerChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is CustomerDetailControl customerDetailControl)
{
var customer = e.NewValue as Customer;
customerDetailControl.txtFirstName.Text = customer?.FirstName ?? "";
customerDetailControl.txtLastName.Text = customer?.LastName ?? "";
customerDetailControl.chkDrinksCoffee.IsChecked = customer?.IsCoffeeDrinker;
}
}
This bound the customerDetailControl to the item selected in customerListView.
Now we implement the data bindings on each control of customerDetailControl.
We give the root UserControl an x:Name
so that we can refer to it in the binding markup extensions of the children as the value of ElementName
.
We can also remove the x:Name
s of the individual controls, as well any trace of the event handlers!
(src)
Controls/CustomerDetailControl.xaml
<UserControl
x:Class="WiredBrainCoffee.UWP.Controls.CustomerDetailControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:WiredBrainCoffee.UWP.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="300"
d:DesignWidth="400"
x:Name="root">
<StackPanel>
<TextBox
Header="First name"
Margin="10"
Text="{Binding ElementName=root,Path=Customer.FirstName,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
/>
<TextBox
Header="Last name"
Margin="10"
Text="{Binding ElementName=root,Path=Customer.LastName,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
/>
<CheckBox
Content="Caffeine fiend"
Margin="10"
IsChecked="{Binding ElementName=root,Path=Customer.FirstName,Mode=TwoWay}"
/>
</StackPanel>
</UserControl>
ViewModel
In the work above, we used the binding markup extension to bind one element to another, using that other element as a data source
<TextBlock Text="{Binding ElementName=root,...}">
We can use the MVVM pattern to assign the object to be bound to customerDetailControl as a data context.
Every UI element has a DataContext
property that can be set, and if it is set to an object then it can be placed there as a default data source that the XAML processor will find as it walks up the element tree. (src)
This will allow us to simplify the markup, removing the x:Name
from the root and the ElementName
from the data bindings of the children.
First we create the ViewModel, which incorporates some of the logic from the former App_SuspendingAsync
and MainPage_LoadedAsync
event handler methods.
The ViewModel can dispose of the references to customerDetailControl and customerListView and replace them with its own Customers property.
ViewModel/MainViewModel.cs
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using WiredBrainCoffee.UWP.DataProviders;
using WiredBrainCoffee.UWP.Models;
namespace WiredBrainCoffee.UWP.ViewModel
{
public class MainViewModel
{
public ObservableCollection<Customer> Customers { get; }
public MainViewModel()
{
_customerDataProvider = new CustomerDataProvider();
Customers = new ObservableCollection<Customer>();
}
private CustomerDataProvider _customerDataProvider;
public async Task LoadAsync()
{
Customers.Clear();
var customers = await _customerDataProvider.LoadCustomersAsync();
foreach (var customer in customers)
{
Customers.Add(customer);
}
}
public async Task SaveAsync()
{
await _customerDataProvider.SaveCustomersAsync(Customers);
}
}
}
To further decouple the ViewModel from the data provider, in order to facilitate testing, we extract an interface from CustomerDataProvider.
DataProviders/ICustomerDataProvider.cs
using System.Collections.Generic;
using System.Threading.Tasks;
using WiredBrainCoffee.UWP.Models;
namespace WiredBrainCoffee.UWP.DataProviders
{
public interface ICustomerDataProvider
{
Task<IEnumerable<Customer>> LoadCustomersAsync();
Task SaveCustomersAsync(IEnumerable<Customer> customers);
}
}
Now we implement an ICustomerDataProvider
parameter to the ViewModel constructor, and remember to pass in a new data provider as an argument implementing that interface.
The private field _customerDataProvider can be removed.
ViewModel/MainViewModel.cs
private ICustomerDataProvider _customerDataProvider;
public MainViewModel(ICustomerDataProvider customerDataProvider)
{
_customerDataProvider = customerDataProvider;
Customers = new ObservableCollection<Customer>();
}
MainPage.xaml.cs
this.ViewModel = new MainViewModel(new CustomerDataProvider());
DataContext = ViewModel;
Finally, since we have a data context on MainPage, we can set it as a source for the ListView
MainPage.xaml
<ListView
ItemsSource="{Binding Customers,Mode=OneWay}"
Grid.Row="1"
x:Name="customerListView"
DisplayMemberPath="FirstName">
Binding the selected customer
At this moment, customerDetailControl is still tied to customerListView's SelectedItem property directly, and not through the ViewModel.
To change this, we implement a SelectedCustomer property on the ViewModel that will be bound to both.
We reuse the Observable base class that implement the INotifyPropertyChanged
interface.
This allows us to use the OnPropertyChanged()
method in the setter of the new SelectedItem property.
We replace the element binding of customerListView with a binding to the SelectedCustomer property in the data context.
ViewModel/MainViewModel.cs
public class MainViewModel : Observable
{
private Customer selectedCustomer;
public Customer SelectedCustomer
{
get { return selectedCustomer; }
set
{
selectedCustomer = value;
OnPropertyChanged();
}
}
// ...
}
MainPage.xaml
<ListView
ItemsSource="{Binding Customers,Mode=OneWay}"
Grid.Row="1"
x:Name="customerListView"
DisplayMemberPath="FirstName"
SelectedItem="{Binding SelectedCustomer,Mode=TwoWay}">
</ListView>
DataTemplate
At this moment, customerListView is being populated by a single property of each Customer - their first name.
If we want to compose more complex information, we can assign DataTemplate to the ListView's ItemTemplate property.
This will create the enclosed controls for each element in the ListView.
Remember to remove the DisplayMemberPath
attribute!
<ListView
ItemsSource="{Binding Customers,Mode=OneWay}"
Grid.Row="1"
x:Name="customerListView"
SelectedItem="{Binding SelectedCustomer,Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding FirstName}"/>
<TextBlock Text="{Binding LastName}" Margin="5 0 0 0" FontWeight="Bold"/>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
x:Bind
There are two data binding types available in XAML (src)
- Binding markup extension resolves the binding path at runtime.
- x:Bind resolves the binding path at compile-time, generating C# code and offering better performance and compile-time errors. You can also step into the compiled code, providing a better debugging experience. x:Bind should generally be preferred, however it is available only in UWP.
Binding markup extension can have several different data sources, depending on defined attributes.
ElementName
Source
RelativeSource
If none of these are defined, then the binding markup extension resolves to the DataContext property.
x:Bind, in contrast, binds only to the parent Page or UserControl element. So any property of MainPage will be accessible, and any property of that object will also be accessible using dot notation.
Most bindings are easily translated between the two types if the ViewModel has already been implemented as a property of MainPage:
public MainPage()
{
this.InitializeComponent;
this.ViewModel = new MainViewModel();
DataContext = ViewModel;
}
<ListView
ItemsSource="{Binding Customers,Mode=OneWay}">
<!-- ...-->
</ListView>
public MainPage()
{
this.InitializeComponent;
this.ViewModel = new MainViewModel();
// DataContext = ViewModel;
}
<ListView
ItemsSource="{x:Bind ViewModel.Customers,Mode=OneWay}">
<!-- ...-->
</ListView>
Notably, the default binding mode of the Binding markup extension is OneWay
x:Bind
is OneTime
, although this can be changed by setting x:DefaultBindMode
on the root element.
<Page>
<Listview ItemsSource="{x:Bind ViewModelCustomers,Mode=OneWay}"/>
</Page>
<Page
x:DefaultBindMode="OneWay">
<Listview ItemsSource="{x:Bind ViewModelCustomers}"/>
</Page>
customDetailControl, which previously used the binding markup extension but set the root element as an explicitly named source property, is notably simplified after replacing with x:Bind
. We can now directly access the user control's property.
<UserControl
x:Class="WiredBrainCoffee.UWP.Controls.CustomerDetailControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:WiredBrainCoffee.UWP.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="300"
d:DesignWidth="400"
x:Name="root">
<StackPanel>
<TextBox
Header="First name"
Margin="10"
Text="{Binding ElementName=root,Path=Customer.FirstName,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
/>
<TextBox
Header="Last name"
Margin="10"
Text="{Binding ElementName=root,Path=Customer.LastName,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
/>
<CheckBox
Content="Caffeine fiend"
Margin="10"
IsChecked="{Binding ElementName=root,Path=Customer.FirstName,Mode=TwoWay}"
/>
</StackPanel>
</UserControl>
<UserControl
x:Class="WiredBrainCoffee.UWP.Controls.CustomerDetailControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:WiredBrainCoffee.UWP.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="300"
d:DesignWidth="400"
>
<StackPanel>
<TextBox
Header="First name"
Margin="10"
Text="{x:Bind Customer.FirstName,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
/>
<TextBox
Header="Last name"
Margin="10"
Text="{x:Bind Customer.LastName,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
/>
<CheckBox
Content="Caffeine fiend"
Margin="10"
IsChecked="{x:Bind Customer.IsCoffeeDrinker,Mode=TwoWay}"
/>
</StackPanel>
</UserControl>
x:Bind can also be implemented in the ListView's ItemTemplate, so long as the x:DataType attribute is set on DataTemplate. We must also remember to set the Mode binding property, since x:Bind's default is OneTime! (src)
<ListView Grid.Row="1" ItemsSource="{x:Bind ViewModel.Customers}" SelectedItem="{x:Bind ViewModel.SelectedCustomer,Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="model:Customer">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{x:Bind FirstName}"/>
<TextBlock Text="{x:Bind LastName}" Margin="5 0 0 0" FontWeight="Bold"/>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
x:Bind can also hide or reveal controls depending on boolean value.
A new boolean property is formed on the ViewModel, and we wire the OnPropertyChanged
event handler to it.
We also bind this value to the Visibility attribute of customerDetailControl.
This will hide the customerDetailControl on application startup before the user selects a customer. (src)
public Customer SelectedCustomer
{
get { return selectedCustomer; }
set
{
if (selectedCustomer != value)
{
selectedCustomer = value;
OnPropertyChanged();
OnPropertyChanged(nameof(IsCustomerSelected));
}
}
}
public bool IsCustomerSelected => SelectedCustomer != null;
<controls:CustomerDetailControl
x:Name="customerDetailControl"
Grid.Row="1" Grid.Column="1"
Customer="{x:Bind ViewModel.SelectedCustomer,Mode=TwoWay}"
Visibility="{x:Bind ViewModel.IsCustomerSelected}"/>
We can also implement a third TextBlock in the ListView's ItemTemplate to show a string depending on the value of the CheckBox.
<ListView
Grid.Row="1"
ItemsSource="{x:Bind ViewModel.Customers}"
SelectedItem="{x:Bind ViewModel.SelectedCustomer,Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="model:Customer">
<StackPanel Orientation="Horizontal">
<TextBlock Text="Dev" Opacity="0.5" Visibility="{x:Bind IsCoffeeDrinker}" Margin="0 0 5 0"/>
<TextBlock Text="{x:Bind FirstName}" />
<TextBlock Text="{x:Bind LastName}" Margin="5 0 0 0" FontWeight="Bold"/>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Toggling visibility
In order to fully implement the logic of the MVVM pattern, we should move functionality that deals with the logic of the app as a whole to the ViewModel. That would include the Add and Delete buttons. (src)
<CommandBar>
<AppBarButton x:Name="AddCustomer" Click="AddCustomer_Click" Label="Add">
<SymbolIcon Symbol="Add"/>
</AppBarButton>
<AppBarButton x:Name="DeleteCustomer" Click="DeleteCustomer_Click" Label="Delete">
<SymbolIcon Symbol="Delete"/>
</AppBarButton>
<AppBarButton x:Name="btn_MoveSideBar" Click="btn_MoveSideBar_Click" Label="Move sidebar">
<SymbolIcon x:Name="btn_MoveSideBar_Symbol" Symbol="AlignRight"/>
</AppBarButton>
</CommandBar>
<CommandBar>
<AppBarButton x:Name="AddCustomer" Click="{x:Bind ViewModel.AddCustomer_Click}" Label="Add">
<SymbolIcon Symbol="Add"/>
</AppBarButton>
<AppBarButton x:Name="DeleteCustomer" Click="{x:Bind ViewModel.DeleteCustomer_Click}" Label="Delete">
<SymbolIcon Symbol="Delete"/>
</AppBarButton>
<AppBarButton x:Name="btn_MoveSideBar" Click="btn_MoveSideBar_Click" Label="Move sidebar">
<SymbolIcon x:Name="btn_MoveSideBar_Symbol" Symbol="AlignRight"/>
</AppBarButton>
</CommandBar>
Styling
You can define a style that has to be used more than once by declaring a Style element on a UserControl's Resources property. (src)
<UserControl>
<UserControl.Resources>
<Style x:Key="myTextBoxStyle" TargetType="TextBox">
<Style.Setters>
<Setter Property="Margin" Value="10"/>
<Setter Property="CornerRadius" Value="10"/>
</Style.Setters>
</Style>
</UserControl.Resources>
</UserControl>
This Style can then be used as a StaticResource, setting the value of the Style attribute.