Most of the time, our design is fixed and we don’t have to change it while it’s executing, but sometimes this is not true: we need to load the files dynamically, due to a lot of factors:
- We have the same business rules for the project but every client needs a different UI
- The design can be different, depending on some conditions
- We are not sure of the design and want to make changes at runtime and see how they work
When this happens, WPF is a huge help, as it can load its design dynamically and load the XAML files at runtime.
Preparing the application
When we want to load the XAML dynamically, we must ensure that all business rules are separated from the UI – we are only loading a new UI, the business rules remain the same. A good fit for that is the MVVM pattern. With it, the business rules are completely separated from the UI and you can use Data Binding to bind the data to the UI. So, when you are designing a dynamic application you should use the MVVM pattern.
One other preparation is to know how will your application be designed: will it run on a single window, changing the content, or will it have multiple windows? If it runs on a single window, you should create a shell with a content control and load all dynamic files in that control. If the application will have multiple windows, you should create a new window every time there is new content.
Once you have the application ready, with the business rules separated from the UI and the kind of UI defined, you can load your XAML dynamically.
Loading the XAML Dynamically
WPF has a simple way to load XAML dynamically. It has a class, XamlReader that has methods to load XAML dynamically and instantiate a new control. It can be used like this:
var mainContent = XamlReader.Parse(@"<Button Width=""85"" Height=""35"" Content=""Test""
xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation""
xmlns:x = ""http://schemas.microsoft.com/winfx/2006/xaml"" />") as Button;
MainGrid.Children.Add(mainContent);
In this example, we are using the Parse method to a XAML component from a string. We could load a XAML file dynamically using the Load method:
using (FileStream fs = new FileStream("Button.xaml", FileMode.Open))
{
mainContent = XamlReader.Load(fs) as Button;
MainGrid.Children.Add(mainContent);
}
Note that both Load and Parse return an object and that must be typecasted to the real type of the control.
Creating a real world dynamic application
In a real world application, we can have different uses to this feature:
- Use the same data with different layouts – in this case, we have a single viewmodel and the views connect to the same data, with different layouts
- Use different formatting options to the same view. This is used when you want to customize the view presentation (color, layout) for different clients
For the first usage, you must create completely different views for the same data and use the MVVM pattern to connect to the data.
For the second usage, you can use the same views, but connect to different Resource Dictionaries, so the new styles can be implemented. In this post, we will implement different views for the same viewmodel.
The project I’ll be using will use a Customers list, loaded from an XML file. The app uses the MVVM pattern (I’ve chosen not to use any MVVM framework, but a simple homemade MVVM framework, with a base ViewModelBase class and a RelayCommand class, to build the infrastructure).
The Customer’s list is loaded in the CustomersViewModel, which has many properties (and commands) to interact with the views:
public class CustomersViewModel : ViewModelBase
{
readonly CustomerRepository customerRepository = new CustomerRepository();
private readonly ObservableCollection<CustomerViewModel> customers;
public CustomersViewModel()
{
var customerViewModels = customerRepository.Customers.Select(c => new CustomerViewModel(c));
customers = new ObservableCollection<CustomerViewModel>(customerViewModels);
customerView = (CollectionView)CollectionViewSource.GetDefaultView(customers);
}
private CustomerViewModel selectedCustomer;
public CustomerViewModel SelectedCustomer
{
get { return selectedCustomer; }
set
{
selectedCustomer = value;
RaisePropertyChanged("SelectedCustomer");
}
}
private ICommand goFirstCommand;
public ICommand GoFirstCommand => goFirstCommand ?? (goFirstCommand = new RelayCommand(GoFirst));
private void GoFirst(object obj)
{
customerView.MoveCurrentToFirst();
SelectedCustomer = (CustomerViewModel)customerView.CurrentItem;
}
private ICommand goPrevCommand;
public ICommand GoPrevCommand => goPrevCommand ?? (goPrevCommand = new RelayCommand(GoPrev));
private void GoPrev(object obj)
{
customerView.MoveCurrentToPrevious();
SelectedCustomer = (CustomerViewModel)customerView.CurrentItem;
}
private ICommand goNextCommand;
public ICommand GoNextCommand => goNextCommand ?? (goNextCommand = new RelayCommand(GoNext));
private void GoNext(object obj)
{
customerView.MoveCurrentToNext();
SelectedCustomer = (CustomerViewModel)customerView.CurrentItem;
}
private ICommand goLastCommand;
public ICommand GoLastCommand => goLastCommand ?? (goLastCommand = new RelayCommand(GoLast));
private void GoLast(object obj)
{
customerView.MoveCurrentToLast();
SelectedCustomer = (CustomerViewModel)customerView.CurrentItem;
}
private string searchText;
public string SearchText
{
get { return searchText; }
set
{
searchText = value;
RaisePropertyChanged("SearchText");
}
}
public ObservableCollection<CustomerViewModel> Customers
{
get { return customers; }
}
private ICommand addCommand;
public ICommand AddCommand
{
get { return addCommand ?? (addCommand = new RelayCommand(AddCustomer, null)); }
}
private void AddCustomer(object obj)
{
var customer = new Customer();
var customerViewModel = new CustomerViewModel(customer);
customers.Add(customerViewModel);
customerRepository.Add(customer);
SelectedCustomer = customerViewModel;
}
private ICommand removeCommand;
public ICommand RemoveCommand
{
get {
return removeCommand ??
(removeCommand = new RelayCommand(RemoveCustomer, c => SelectedCustomer != null));
}
}
private void RemoveCustomer(object obj)
{
customerRepository.Remove(SelectedCustomer.Customer);
customers.Remove(SelectedCustomer);
SelectedCustomer = null;
}
private ICommand saveCommand;
public ICommand SaveCommand
{
get { return saveCommand ?? (saveCommand = new RelayCommand(SaveData, null)); }
}
private void SaveData(object obj)
{
customerRepository.Commit();
}
private ICommand searchCommand;
private ICollectionView customerView;
public ICommand SearchCommand
{
get
{
if (searchCommand == null)
searchCommand = new RelayCommand(SearchData, null);
return searchCommand;
}
}
private void SearchData(object obj)
{
customerView.Filter = !string.IsNullOrWhiteSpace(SearchText) ?
c => ((CustomerViewModel)c).Country.ToLower()
.Contains(SearchText.ToLower()) :
(Predicate<object>)null;
}
}
The main property is Customer, that has the list of CustomerViewModels that will be shown in the master views and SelectedCustomer, that has the selected customer that will be edited in the details views. There are a lot of commands to filter the list, move the selected record, add, remove and save the data.
The CustomerViewModel class is:
public class CustomerViewModel : ViewModelBase
{
public Customer Customer { get; private set; }
public CustomerViewModel(Customer cust)
{
Customer = cust;
}
public string CustomerId
{
get { return Customer.CustomerId; }
set
{
Customer.CustomerId = value;
RaisePropertyChanged("CustomerId");
}
}
public string CompanyName
{
get { return Customer.CompanyName; }
set
{
Customer.CompanyName = value;
RaisePropertyChanged("CompanyName");
}
}
public string ContactName
{
get { return Customer.ContactName; }
set
{
Customer.ContactName = value;
RaisePropertyChanged("ContactName");
}
}
public string ContactTitle
{
get { return Customer.ContactTitle; }
set
{
Customer.ContactTitle = value;
RaisePropertyChanged("ContactTitle");
}
}
public string Region
{
get { return Customer.Region; }
set
{
Customer.Region = value;
RaisePropertyChanged("Region");
}
}
public string Address
{
get { return Customer.Address; }
set
{
Customer.Address = value;
RaisePropertyChanged("Address");
}
}
public string City
{
get { return Customer.City; }
set
{
Customer.City = value;
RaisePropertyChanged("City");
}
}
public string Country
{
get { return Customer.Country; }
set
{
Customer.Country = value;
RaisePropertyChanged("Country");
}
}
public string PostalCode
{
get { return Customer.PostalCode; }
set
{
Customer.PostalCode = value;
RaisePropertyChanged("PostalCode");
}
}
public string Phone
{
get { return Customer.Phone; }
set
{
Customer.Phone = value;
RaisePropertyChanged("Phone");
}
}
public string Fax
{
get { return Customer.Fax; }
set
{
Customer.Fax = value;
RaisePropertyChanged("Fax");
}
}
}
It doesn’t do anything but expose the Customer’s properties. With this infrastructure in place, I can create my view loading. I did this in the code behind, the code is very simple, it just sets the data context for the window and loads the correct file, depending on the selection of a combobox:
public MainWindow()
{
InitializeComponent();
DataContext = new CustomersViewModel();
}
private void SelectedViewChanged(object sender, SelectionChangedEventArgs e)
{
var viewIndex = (sender as ComboBox).SelectedIndex;
FrameworkElement view = null;
switch (viewIndex)
{
case 0:
view = LoadView("masterdetail.xaml");
break;
case 1:
view = LoadView("detail.xaml");
break;
case 2:
view = LoadView("master.xaml");
break;
}
MainContent.Content = view;
}
private FrameworkElement LoadView(string fileName)
{
using (FileStream fs = new FileStream(fileName, FileMode.Open))
{
return XamlReader.Load(fs) as FrameworkElement;
}
}
The XAML for the main window is:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="40"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<ComboBox Height="30" Width="200" SelectionChanged="SelectedViewChanged" HorizontalAlignment="Left" Margin="5">
<ComboBoxItem>Master/Detail</ComboBoxItem>
<ComboBoxItem>Detail</ComboBoxItem>
<ComboBoxItem>Master</ComboBoxItem>
</ComboBox>
<ContentControl Grid.Row="1" x:Name="MainContent"/>
</Grid>
As you can see, the main window is very simple, it just has the combobox to select the view and a ContentControl, where the new view is loaded. An example of the dynamic view is the Master/Detail view:
<UserControl xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="600" d:DesignWidth="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="40" />
<RowDefinition Height="*" />
<RowDefinition Height="2*" />
<RowDefinition Height="50" />
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal">
<TextBlock Text="Country" VerticalAlignment="Center" Margin="5"/>
<TextBox Height="25"
VerticalAlignment="Center" Margin="5,3" Width="250" Text="{Binding SearchText, Mode=TwoWay}" />
<Button Content="Search" Width="75" Height="25" Margin="10,5" VerticalAlignment="Center"
Command="{Binding SearchCommand}" />
</StackPanel>
<DataGrid AutoGenerateColumns="False" x:Name="master" CanUserAddRows="False" CanUserDeleteRows="True" Grid.Row="1"
ItemsSource="{Binding Customers}" SelectedItem="{Binding SelectedCustomer, Mode=TwoWay}" >
<DataGrid.Columns>
<DataGridTextColumn x:Name="customerIDColumn" Binding="{Binding Path=CustomerId}" Header="Customer ID" Width="80" />
<DataGridTextColumn x:Name="companyNameColumn" Binding="{Binding Path=CompanyName,ValidatesOnDataErrors=True}" Header="Company Name" Width="300" />
<DataGridTextColumn x:Name="cityColumn" Binding="{Binding Path=City}" Header="City" Width="100" />
<DataGridTextColumn x:Name="countryColumn" Binding="{Binding Path=Country}" Header="Country" Width="100" />
</DataGrid.Columns>
</DataGrid>
<Grid DataContext="{Binding SelectedCustomer}" Grid.Row="2">
<Grid Name="grid1" >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Label Content="Customer Id:" Grid.Column="0" Grid.Row="0" Margin="3" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Grid.Row="0" Margin="3" Name="customerIdTextBox" Text="{Binding Path=CustomerId, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" />
<Label Content="Company Name:" Grid.Column="0" Grid.Row="1" Margin="3" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Grid.Row="1" Margin="3" Name="companyNameTextBox" Text="{Binding Path=CompanyName, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" />
<Label Content="Contact Name:" Grid.Column="0" Grid.Row="2" Margin="3" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Grid.Row="2" Margin="3" Name="contactNameTextBox" Text="{Binding Path=ContactName, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" />
<Label Content="Contact Title:" Grid.Column="0" Grid.Row="3" Margin="3" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Grid.Row="3" Margin="3" Name="contactTitleTextBox" Text="{Binding Path=ContactTitle, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" />
<Label Content="Address:" Grid.Column="0" Grid.Row="4" HorizontalAlignment="Left" Margin="3" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Grid.Row="4" Margin="3" Name="addressTextBox" Text="{Binding Path=Address, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" />
<Label Content="City:" Grid.Column="0" Grid.Row="5" Margin="3" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Grid.Row="5" Margin="3" Name="cityTextBox" Text="{Binding Path=City, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" />
<Label Content="Postal Code:" Grid.Column="0" Grid.Row="6" Margin="3" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Grid.Row="6" Margin="3" Name="postalCodeTextBox" Text="{Binding Path=PostalCode, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" />
<Label Content="Region:" Grid.Column="0" Grid.Row="7" Margin="3" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Grid.Row="7" Margin="3" Name="regionTextBox" Text="{Binding Path=Region, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" />
<Label Content="Country:" Grid.Column="0" Grid.Row="8" Margin="3" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Grid.Row="8" Margin="3" Name="countryTextBox" Text="{Binding Path=Country, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" />
<Label Content="Phone:" Grid.Column="0" Grid.Row="9" Margin="3" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Grid.Row="9" Margin="3" Name="phoneTextBox" Text="{Binding Path=Phone, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" />
<Label Content="Fax:" Grid.Column="0" Grid.Row="10" Margin="3" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Grid.Row="10" Margin="3" Name="faxTextBox" Text="{Binding Path=Fax, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" />
</Grid>
</Grid>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="5" Grid.Row="3">
<Button Width="75" Height="25" Margin="5" Content="Add" Command="{Binding AddCommand}"/>
<Button Width="75" Height="25" Margin="5" Content="Remove" Command="{Binding RemoveCommand}"/>
<Button Width="75" Height="25" Margin="5" Content="Save" Command="{Binding SaveCommand}" />
</StackPanel>
</Grid>
</UserControl>
There are some things to note, here:
- There is no x:Class attribute in the UserControl
- There is no code behind at all for the XAML. As this is a loose file, there should not be any cs file tied to it (and that’s a reason for not having the x:Class attribute)
- I added the XAML files to the project, setting the Build Action to None and the Copy to Output Directory to Copy If Newer. This is an optional step, the files don’t need to be added to the project, they only need to be available on the executable directory at runtime
With this setting, you can run the application and get something like in the figure below:
You can see that everything works, both the data bindings and the commands. These files can be changed at will and they will be linked to the CustomersViewModel.
Conclusions
This kind of structure is very flexible and easy to use. Some uses I envision for it is to create different views for the same data, use layout customization for different clients, allow designers to be really free regarding to the design of the app, or create globalized apps (you can have different directories for each language, and each directory can have a translated view).
The next article will show subtler changes, when you only want to change the style of the controls.
The full code for the article is in https://github.com/bsonnino/DynamicXamlViews
I know this is a late read, but have a question. I have a WPF app that does something similar to this but adds a new Tab with a View in it to a Tab List. Works great with one exception. One of my views had a 3rd party control that can only be called and init’d from the code behind. The View, View.xaml.cs and ViewModel all work together to make the 3rd party component work. If this View is used on application start, it works fine, but adding another during live use, I get a blank screen with the ViewModel name it. The codebehind never fires. Know of any solutions for this?
You will have to have the code for calling and initializing the 3rd party control in your app (you cannot load it dinamically, unless you compile it at runtime, but that is another post :-)), then call this code when you load the view.
You can see that the code for the buttons is already there, in the CustomersViewModel and it’s bound at runtime. You can add the code you need to the code behind of MainWindow, after loading the View:
switch (viewIndex) { case 0: view = LoadView("masterdetail.xaml"); break; case 1: view = LoadView("detail.xaml"); InitializeThirdPartyControl(view); break; case 2: view = LoadView("master.xaml"); break; } MainContent.Content = view;
Thanks, very helpful !