Once upon a time, in the old days, when WPF was a little new baby and people needed a tool to fiddle around with the new design language, Microsoft released a tool named XAMLPad, and things were all good: you didn’t need Visual Studio to try your designs, you could learn XAML without having to start a new project and you could try as much as you wanted, the UI changed as you changed the XAML code.

Then, time has passed. WPF is old news, many new XAML platforms appeared and XAMLPad faded out and all that remained from it was an old page.

Although it was not there, the need remained: how to do quick tests on your XAML? How to do them for the new platforms, like UWP? Then, a Microsoft Engineer decided to revive this and XAML Studio was born.

XAML Studio is a UWP app that allows you to fiddle with the XAML code in UWP and even debug the bindings! You can get the app from the Windows Store and install it in your machine for free.

Once you install and open it, you will get a window like this:

The app has a tabbed interface and you can create a new page and start typing your code. When you create a new page, a new tab will open, like this one:

You can start changing things immediately and the changes will reflect on the top pane. For example, if you change the color of the first run of text to Navy, the top pane will reflect that. If  you make a typing mistake, a pink box will be shown in the top pane, indicating the compilation error:

The last icon in the navigation bar is the toolbox. When you select it, you will have all available components to insert in your code. You can filter the items you want by typing in the search box at the top:

In this post, I’ve shown how to use the NavigationView in UWP. With XAML Studio, we don’t need to use Visual Studio to test the code, we can just type it in the code window and the UI will be shown in the top pane. If you type something like:

<Page
  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" xmlns:winui="using:Microsoft.UI.Xaml.Controls">
  <winui:NavigationView >
    <winui:NavigationView.MenuItems>
      <winui:NavigationViewItem Content="Main" Icon="Home" />
    </winui:NavigationView.MenuItems>
    <Grid Padding="40">
      <TextBlock>
        <Run FontSize="24" Foreground="Navy">Get Started with XAML Studio</Run><LineBreak/>
        <Run> Modify this text below to see a live preview.</Run>
      </TextBlock>
    </Grid>
  </winui:NavigationView>
</Page>

You will get something like this:

You can even use controls from Telerik or from the Microsoft Community Toolkit. For example, if you want to add a Radial Gauge to your UI, you can use something like this:

<Page
  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" xmlns:winui="using:Microsoft.UI.Xaml.Controls" 
  xmlns:controls="using:Microsoft.Toolkit.Uwp.UI.Controls">
  <winui:NavigationView >
    <winui:NavigationView.MenuItems>
      <winui:NavigationViewItem Content="Main" Icon="Home" />
    </winui:NavigationView.MenuItems>
    <Grid Padding="40">
      <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="*" />
      </Grid.RowDefinitions>
      <TextBlock>
        <Run FontSize="24" Foreground="Navy">Get Started with XAML Studio</Run><LineBreak/>
        <Run> Modify this text below to see a live preview.</Run>
      </TextBlock>
      <controls:RadialGauge Value="25" Unit="mph" NeedleBrush="Red"
                                  TrailBrush="Green"
                                  Grid.Row="1" Width="200" 
                                  Height="200" Foreground="Green"/>
    </Grid>
  </winui:NavigationView>
</Page>

One thing very interesting that can be done is to use data bindings in your code. You can create a Json Data Source or use a remote data source to bind to your data. For example, if you go to the second icon (Data Source) and add something like this:

{
  'Title' : 'Using XAML Studio',
  'Subtitle' : 'By Bruno Sonnino',
  'GaugeValue' : 35,
  'GaugeUnit' : 'km/h',
  'GaugeNeedle' : 'DarkGreen'
}

You can bind to your code with something like this:

<Page
  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" xmlns:winui="using:Microsoft.UI.Xaml.Controls" 
  xmlns:controls="using:Microsoft.Toolkit.Uwp.UI.Controls">
  <winui:NavigationView >
    <winui:NavigationView.MenuItems>
      <winui:NavigationViewItem Content="Main" Icon="Home" />
    </winui:NavigationView.MenuItems>
    <Grid Padding="40">
      <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="*" />
      </Grid.RowDefinitions>
      <TextBlock>
        <Run FontSize="24" Foreground="Navy" Text="{Binding Title}"/><LineBreak/>
        <Run Text="{Binding Subtitle}"/>
      </TextBlock>
      <controls:RadialGauge Value="{Binding GaugeValue}" Unit="{Binding GaugeUnit}" 
                                  NeedleBrush="{Binding GaugeNeedle}"
                                  TrailBrush="Green"
                                  Grid.Row="1" Width="200" 
                                  Height="200" Foreground="Green"/>
    </Grid>
  </winui:NavigationView>
</Page>

You can also bind to a remote data source, using an URL to a JSON service. For example, we can bind to the European Currency Exchange service to get the quotes of the currencies against the Euro. Just go to the Data Source option and select the Remote Data Source and enter this URL:

https://api.exchangeratesapi.io/latest

Then you can show the values for the currencies in a DataGrid with this code:

<Page
  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" xmlns:controls="using:Microsoft.Toolkit.Uwp.UI.Controls">
  <Grid Padding="40">
    <Grid.RowDefinitions>
      <RowDefinition Height="Auto" />
      <RowDefinition Height="*" />
    </Grid.RowDefinitions>
    <StackPanel Orientation="Horizontal" Margin="0,10">
      <TextBlock Text="Base" Margin="5,0"/>
      <TextBlock Text="{Binding base}" Margin="5,0"/>
      <TextBlock Text="Date" Margin="5,0"/>
      <TextBlock Text="{Binding date}" Margin="5,0"/>
    </StackPanel>
    <controls:DataGrid ItemsSource="{Binding rates}" Grid.Row="1"/>
  </Grid>
</Page>

One nice feature is that you can debug your bindings. In the code window, you can notice a green background in the bindings. If you hover the mouse in the binding, it will show you the current value:

When you go to the third menu item (Debug Bindings), you can even get more information, like the history for the binding or the values for all bindings at once:

Conclusions

As you can see XAML Studio has a lot of features for who wants to try some XAML code or even learn XAML. You don’t need to fire Visual Studio to test your XAML code and see what happens if you change something. It’s still a work in progress and has a lot of room for improvement, but with no doubt it’s a very nice tool to have in your toolbox.

One of the features I really like in Visual Studio while I am developing is the Edit and Continue feature. Just add a breakpoint in the code, edit the code and maybe even reposition the run pointer and you can try new features, set some properties or add new code and run, without having to restart the program.

This is really a timesaver, I’ve used it a lot of time and it has saved me hours of development. But, when I was doing some WPF or UWP development, the UI development was slowed down, because every time I needed to make some change to the design, I had to stop the program, make a change and re-run again.

Things were worse because the designer was less than optimal and, for complex designs, with lots of resources, animations and so on, I couldn’t see what was happening at design time. When the live visual tree was introduced, this was made better: I could see the visual tree and interact with it, setting some properties at runtime. The pitfall in this is that I needed to make the notes of what was changed, because the changes would be lost when the program stopped.

Then came XAML Edit and Continue. What a difference! The name of the feature is misleading, because it’s not Edit and Continue (I would suggest XAML Runtime Edititng). You don’t need to stop the program and set a breakpoint anywhere – just edit the code while it’s running and see the changes happen!

This is a great feature, you can start from a blank page and start adding data to it. If you have a ViewModel attached to the View, you can also use data binding to show the ViewModel data. One thing should be noted, here: if you are using x:Bind to make the binding to the data, it won’t work at runtime, because this binding is resolved at compile time and it won’t be available at runtime.

I use this feature when I want to try something in my UI – that can be something new or something that can be improved in my code. For example, if I have this CommandBar in my code (code courtesy of https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls.commandbar), I can try to tweak its options directly in the code and see what happens.

<Page.TopAppBar>
    <CommandBar>
        <AppBarToggleButton Icon="Shuffle" Label="Shuffle" Click="AppBarButton_Click"/>
        <AppBarToggleButton Icon="RepeatAll" Label="Repeat" Click="AppBarButton_Click"/>
        <AppBarSeparator/>
        <AppBarButton Icon="Back" Label="Back" Click="AppBarButton_Click"/>
        <AppBarButton Icon="Stop" Label="Stop" Click="AppBarButton_Click"/>
        <AppBarButton Icon="Play" Label="Play" Click="AppBarButton_Click"/>
        <AppBarButton Icon="Forward" Label="Forward" Click="AppBarButton_Click"/>

        <CommandBar.SecondaryCommands>
            <AppBarButton Icon="Like" Label="Like" Click="AppBarButton_Click"/>
            <AppBarButton Icon="Dislike" Label="Dislike" Click="AppBarButton_Click"/>
        </CommandBar.SecondaryCommands>

        <CommandBar.Content>
            <TextBlock Text="Now playing..." Margin="12,14"/>
        </CommandBar.Content>
    </CommandBar>
</Page.TopAppBar>

For example, I want to know what happens when the IsCompact  property of an AppBarButton is set to False. I can just add it to the code and see what happens:

Then I removed the tag from the code. Instead of seeing the UI be restored to the default, the IsCompact property remained to False. You should be aware that once you set a property and remove it from the code, the property will still be set to the value until you reset it explicitely or rerun the program. Be aware of that or you will have some hard time to see why things aren’t the way you expect :-).

If you are using a resource dictionary, you can also change the styles there and the UI will change accordingly. For example, I have a Resource Dictionary like this:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Style TargetType="CommandBar">
        <Setter Property="Foreground" Value="White"/>
        <Setter Property="Background" Value="Gray"/>
    </Style>
</ResourceDictionary>

I add it to the app resources with something like this:

<Application
    x:Class="App1.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Application.Resources>
        <ResourceDictionary Source="ResourceDict.xaml"/>
    </Application.Resources>
</Application>

When I take a look at the UI, I see something like this:

As you can see, the content is white, while the buttons are black, and surely that’s not what I want. I can add a new style like this:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Style TargetType="CommandBar">
        <Setter Property="Foreground" Value="White"/>
        <Setter Property="Background" Value="Gray"/>
    </Style>
   <Style TargetType="SymbolIcon">
       <Setter Property="Foreground" Value="White"/>
   </Style>
</ResourceDictionary>

And the UI reflects the new style for the icons:

One other area that you can tweak is when you are designing a responsive view. You will have something like this:

 <VisualStateManager.VisualStateGroups>
            <VisualStateGroup>
                <VisualState>
                    <!-- VisualState to be triggered when window width is >=720 effective pixels -->
                    <VisualState.StateTriggers>
                        <AdaptiveTrigger MinWindowWidth="720" />
                    </VisualState.StateTriggers>
                    <VisualState.Setters>
                        <!-- Widest possible layout moves some elements around to optimize for more available width 
                        and keeps SplitView pane always showing inline -->
                        <Setter Target="MySplitView.DisplayMode" Value="Inline" />
                        <Setter Target="MySplitView.IsPaneOpen" Value="True" />
                        <Setter Target="BackgroundCombo.(RelativePanel.RightOf)" Value="BackgroundImage" />
                        <Setter Target="BackgroundCombo.(RelativePanel.AlignTopWith)" Value="BackgroundImage" />
                        <Setter Target="BackgroundCombo.(RelativePanel.AlignLeftWith)" Value="FitCombo" />
                        <Setter Target="PictureLabel.(RelativePanel.Below)" Value="BackgroundImage" />
                        <Setter Target="FitCombo.(RelativePanel.RightOf)" Value="PicturesPanel" />
                        <Setter Target="FitCombo.(RelativePanel.AlignTopWith)" Value="PictureLabel" />
                    </VisualState.Setters>
                </VisualState>
                <VisualState>
                    <!-- VisualState to be triggered when window width is >=548 and <720 effective pixels -->
                    <VisualState.StateTriggers>
                        <AdaptiveTrigger MinWindowWidth="548" />
                    </VisualState.StateTriggers>
                    <VisualState.Setters>
                        <!-- For intermediate window widths as well as most phones on landscape orientation, 
                        this state keeps primary layout narrow while showing the splitview pane to take advantage of more available width than narrow layout -->
                        <Setter Target="MySplitView.DisplayMode" Value="Inline" />
                        <Setter Target="MySplitView.IsPaneOpen" Value="True" />
                        <Setter Target="BackgroundCombo.(RelativePanel.Below)" Value="BackgroundImage" />
                        <Setter Target="PictureLabel.(RelativePanel.Below)" Value="BackgroundCombo" />
                        <Setter Target="FitCombo.(RelativePanel.Below)" Value="BrowseButton" />
                    </VisualState.Setters>
                </VisualState>
                <VisualState>
                    <!-- VisualState to be triggered when window width is >=0 and <548 effective pixels -->
                    <VisualState.StateTriggers>
                        <AdaptiveTrigger MinWindowWidth="0" />
                    </VisualState.StateTriggers>
                    <VisualState.Setters>
                        <!-- For the most narrow windows and phones in portrait orientation, this state collapses the SplitView pane into overlay mode
                        and adds dynamic RelativePanel constraints that puts all elements stacked below each other -->
                        <Setter Target="MySplitView.DisplayMode" Value="Overlay" />
                        <Setter Target="MySplitView.IsPaneOpen" Value="False" />
                        <Setter Target="BackgroundCombo.(RelativePanel.Below)" Value="BackgroundImage" />
                        <Setter Target="PictureLabel.(RelativePanel.Below)" Value="BackgroundCombo" />
                        <Setter Target="FitCombo.(RelativePanel.Below)" Value="BrowseButton" />
                    </VisualState.Setters>
                </VisualState>
            </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>

You can change the triggers for the states, changing the windows sizes or the display for the many states. That is something that will save you a lot of time when developing responsive designs!

One thing in which this feature really shines is in testing animations – if you are using Visual Studio to create XAML animations, you are out of luck to test them, there is no way to test them in design time – you must use Blend for that. With this feature, you can create, tweak and test your animations without having to restart your program. For example, if you have this UI:

<Page.Resources>
    <Storyboard x:Name="myStoryboard">
        <DoubleAnimation
            Storyboard.TargetName="MyAnimatedRectangle"
            Storyboard.TargetProperty="Opacity"
            From="1.0" To="0.0" Duration="0:0:2"
            AutoReverse="True" />
    </Storyboard>
</Page.Resources>
<Grid>
    <Rectangle x:Name="MyAnimatedRectangle" 
               Width="300" Height="200" Fill="Blue"/>
</Grid>

And your animation is started by a button click like this one:

private void AppBarButton_Click(object sender, RoutedEventArgs e)
{
    myStoryboard.Begin();
}

You can run your program and change the animation parameters at runtime. Every time you click the button, the new animation will be run and you will be able to see its effects. Nice, no?

Conclusions

This feature is one of the nicest ones when it comes to XAML development. If you are a beginner learning XAML, you can use it to learn and see what happens when you change things, thus speeding up your learning curve, and if you are an experienced XAML designer, you can design your views interactively, using all the features you want. You just need to create a blank app and start designing!

The source code for this article is at https://github.com/bsonnino/XamlEditAndContinue

Some time ago, I’ve written this post about loading XAML dynamically. You can use this in many different ways:

  • store the views in a database
  • load the views from a server
  • change views depending on the user

Using that method, you can change the view, but there is a pitfall: all code must be loaded when the code is initialized. To change anything, you must recompile all the code. That way, the Views you are loading must have properties that match the ones with the ViewModels already in the code.

So, I started to think a way to load the XAML files and its corresponding ViewModel dynamically. Fortunately, C# provides us a mechanism to compile C# code at runtime and execute it. That’s great for our needs.

Compiling C# code at runtime

Compiling code at runtime is not new, you can compile code using the CSharpCodeProvider class with some code like this one:

        static void Main(string[] args)
        {
            CSharpCodeProvider provider = new CSharpCodeProvider();
            CompilerParameters parameters = new CompilerParameters {GenerateInMemory = true};
            parameters.ReferencedAssemblies.Add("System.dll");
            var code =
@"  
using System;
namespace DynamicCompile
{
    public class DynamicCode
    {
        public void Execute()
        {
            Console.WriteLine(""Hello World"");
        }
    }
}";
            CompilerResults results = provider.CompileAssemblyFromSource(
                parameters, code);
            if (!results.Errors.HasErrors)
            {
                var type = results.CompiledAssembly.GetType("DynamicCompile.DynamicCode");
                var method = type.GetMethod("Execute", BindingFlags.Public | BindingFlags.Instance);
                var cls = Activator.CreateInstance(type);
                method?.Invoke(cls, null);
            }
            else
            {
                foreach (CompilerError error in results.Errors)
                {
                    Console.WriteLine(error.ErrorText);
                }
            }
        }

If you execute this code, you will see “Hello World” written in the console. To use it, you must add the namespaces System.CodeDom.Compiler and System. CSharp. This code will use the legacy compiler and will compile code before C#6. If you want to compile C#6 code or newer, you will have to use the Roslyn compiler. To do that, you must add the Microsoft.CodeDom.Providers.DotNetCompilerPlatform NuGet package and add the Microsoft.CodeDom.Providers.DotNetCompilerPlatform namespace. That way, the same code will be able to compile C#6 or later:

        static void Main(string[] args)
        {
            CSharpCodeProvider provider = new CSharpCodeProvider();
            CompilerParameters parameters = new CompilerParameters {GenerateInMemory = true};
            parameters.ReferencedAssemblies.Add("System.dll");
            var code =
@"  
using System;
namespace DynamicCompile
{
    public class DynamicCode
    {
        const string str = ""World"";
        public void Execute()
        {
            Console.WriteLine($""Hello {str}"");
        }
    }
}";
            CompilerResults results = provider.CompileAssemblyFromSource(
                parameters, code);
            if (!results.Errors.HasErrors)
            {
                var type = results.CompiledAssembly.GetType("DynamicCompile.DynamicCode");
                var method = type.GetMethod("Execute", BindingFlags.Public | BindingFlags.Instance);
                var cls = Activator.CreateInstance(type);
                method?.Invoke(cls, null);
            }
            else
            {
                foreach (CompilerError error in results.Errors)
                {
                    Console.WriteLine(error.ErrorText);
                }
            }
        }

As you can see, the code above gives the same result, but uses the string interpolation, available in C#6. With this knowledge, we can work on loading the ViewModel code at runtime.

Loading and Running the ViewModel code at runtime

We’ve seen how to compile and run some code at runtime. With this knowledge, we can create a loose file with the ViewModel corresponding to the loaded XAML, load it and compile it at runtime. One extra bonus is that the View doesn’t know where do the data comes from, so we can load the ViewModel, compile it, instantiate an instance of the compiled class and assign this instance as the DataContext for the View.

As a sample, we will create a simple WPF project that will load a XAML file and its ViewModel into the main screen. Create a new blank WPF project and call it DynamicXAMLAndVM. In the XAML for MainPage.xaml, add this code:

<Grid>
   <ContentControl x:Name="WndContent"/> 
</Grid>

In the code behind for MainPage.xaml.cs, add this code:

public MainWindow()
{
    InitializeComponent();
    using (FileStream fs = new FileStream("CustView.xaml", FileMode.Open))
    {
        WndContent.Content= XamlReader.Load(fs) as FrameworkElement;
        var vmType = LoadViewModel("CustViewModel");
        if (vmType == null)
            MessageBox.Show(_errors, "Errors in compile");
        else
            DataContext = Activator.CreateInstance(vmType);
    }
    
}

private string _errors;
public Type LoadViewModel(string viewModelName)
{
    CSharpCodeProvider provider = new CSharpCodeProvider();
    CompilerParameters parameters = new CompilerParameters {GenerateInMemory = true};
    parameters.ReferencedAssemblies.Add("System.dll");
    parameters.ReferencedAssemblies.Add(Assembly.GetEntryAssembly().Location);
    var code = File.ReadAllText(viewModelName + ".cs");
    CompilerResults results = provider.CompileAssemblyFromSource(parameters, code);
    if (results.Errors.HasErrors)
    {
        _errors = "";
        foreach (CompilerError error in results.Errors)
        {
            _errors += $"Error #{error.ErrorNumber}: {error.ErrorText}\n";
        }

        return null;
    }
    else
    {
        Assembly assembly = results.CompiledAssembly;
        return assembly.GetType($"DynamicXamlAndVM.{viewModelName}");
    }
}

In the constructor, we load the XAML file and assign it to the Content property of the ContentControl. Then we call LoadViewModel, that will load the ViewModel and return its type. If the type is not null, we create an instance of the class with Activator.CreateInstance and assign it to the DataContext of the View. That’s all that’s needed to load the XAML and the ViewModel.

LoadViewModel will create a CSharpCodeProvider and will compile the code. If there are no errors in the code, it will return the parsed type. There is something that should be noticed here: we are referencing two assemblies for the compilation: System.dll and the current assembly. I’m doing that because the compiled class will be located in a separate assembly and won’t have access to the code in the current one.

I am not using any MVVM framework. Instead, I have created a new class, RelayCommand, based on Josh Smith’s code found here.

public class RelayCommand : ICommand
{
    private readonly Predicate<object> _canExecute;
    private readonly Action<object> _execute;

    public RelayCommand(Action<object> execute, Predicate<object> canExecute)
    {
        _canExecute = canExecute;
        _execute = execute;
    }

    public event EventHandler CanExecuteChanged
    {
        add => CommandManager.RequerySuggested += value;
        remove => CommandManager.RequerySuggested -= value;
    }

    public bool CanExecute(object parameter)
    {
        return _canExecute(parameter);
    }

    public void Execute(object parameter)
    {
        _execute(parameter);
    }
}

To access this code in the main assembly, you must add it to the Referenced Assemblies. If you are using a third party MVVM framework, you must add all its assemblies to the referenced assemblies.

The next step is to create the View and the ViewModel. Create a new file and name it CustView.xaml. In the properties page, set its BuildAction to None and the Copy to output directory to Copy if newer. Then, add this code:

<Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
    <Grid.RowDefinitions>
        <RowDefinition Height="40"/>
        <RowDefinition Height="40"/>
        <RowDefinition Height="40"/>
        <RowDefinition Height="40"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition Width="2*"/>
    </Grid.ColumnDefinitions>
    <TextBlock Grid.Column="0" Grid.Row="0" Text="ID" Margin="5"/>
    <TextBlock Grid.Column="0" Grid.Row="1" Text="Name" Margin="5"/>
    <TextBlock Grid.Column="0" Grid.Row="2" Text="Address" Margin="5"/>
    <TextBlock Grid.Column="0" Grid.Row="3" Text="City" Margin="5"/>
    <TextBox Grid.Column="1" Grid.Row="0" Text="{Binding ID}" Margin="5"/>
    <TextBox Grid.Column="1" Grid.Row="1" Text="{Binding Name}" Margin="5"/>
    <TextBox Grid.Column="1" Grid.Row="2" Text="{Binding Address}" Margin="5"/>
    <TextBox Grid.Column="1" Grid.Row="3" Text="{Binding City}" Margin="5"/>
    <Button Grid.Column="1" Grid.Row="4" Command="{Binding ClearCommand}" Margin="5" 
            HorizontalAlignment="Right" VerticalAlignment="Bottom" Content="Clear Data"
            Width="65" Height="35" />
</Grid>

As you can see, it’s a simple grid with some TextBoxes and a Button. The Text property of the TextBoxes are bound to properties in the DataContext. Thanks to the data binding, the View does not know from where the data comes from. We just need to assign a class that has the properties ID, Name, Address, City and a command named ClearCommand. The ViewModel is also a loose cs file. Create a new file and name it CustViewModel.cs, setting its BuildAction to None and the Copy to output directory to Copy if newer. Then, add this code:

class CustViewModel : INotifyPropertyChanged
{
    private string _id;
    private string _name;
    private string _address;
    private string _city;
    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = nul
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    public CustViewModel()
    {
        ID = "007";
        Name = "James Bond";
        Address = "MI6";
        City = "London";

    }
    public string ID
    {
        get => _id;
        set
        {
            _id = value;
            OnPropertyChanged();
        }
    }

    public string Name
    {
        get => _name;
        set
        {
            _name = value;
            OnPropertyChanged();
        }
    }

    public string Address
    {
        get => _address;
        set
        {
            _address = value;
            OnPropertyChanged();
        }
    }

    public string City
    {
        get => _city;
        set
        {
            _city = value;
            OnPropertyChanged();
        }
    }

    public ICommand ClearCommand => new RelayCommand(
        o =>
        {
            ID = "";
            Name = "";
            Address = "";
            City = "";
        },
        o => true);
}

This is a simple ViewModel, which has the properties needed for the View and a command that will clear all bound properties when it’s executed. That’s all that is needed to load a View with its ViewModel and add it to the main screen. When you run the app, you will see something like this:

As you can see, all the data is loaded into the TextBoxes and, when you click on the button, the data is cleared. Nice, no? This code is tied to the files in the project. Now, we can streamline this process, refactoring the code and allowing it to call any file added in the project. For that, we will use some rules:

  • All the views will be in he Views folder
  • All the viewmodels will be in the ViewModels folder and will have the same name of the View
  • The namespace for all viewmodels will be DynamicVM

Refactoring the code

In the project, create a new folder and name it Views and move the CustView.xaml file to it. Rename the file to Cust.xaml. Then, create a new folder and name it ViewModels and move the CustViewModel.cs file to it. Rename it to Cust.cs. In the file, rename the class to Cust and rename the namespace to DynamicVM.

Create a new class named DynamicContentLoader and add this code:

using System;
using System.CodeDom.Compiler;
using System.IO;
using System.Reflection;
using System.Windows;
using System.Windows.Markup;
using Microsoft.CodeDom.Providers.DotNetCompilerPlatform;

namespace DynamicXamlAndVMRefactored
{
    public class DynamicContentLoader
    {
        public static string Errors => _errors;
        public static FrameworkElement Load(string viewName)
        {
            var viewPath = $"Views\\{viewName}.xaml";
            if (!File.Exists(viewPath))
                return null;
            try
            {
                using (FileStream fs = new FileStream(viewPath, FileMode.Open))
                {
                    var result = XamlReader.Load(fs) as FrameworkElement;
                    if (result == null)
                        return null;
                    var viewModelPath = $"ViewModels\\{viewName}.cs";
                    if (File.Exists(viewModelPath))
                    {
                        var vmType = LoadViewModel(viewModelPath, viewName);
                        if (vmType != null)
                        {
                            result.DataContext = Activator.CreateInstance(vmType);
                        }
                    }

                    return result;
                }
            }
            catch (Exception e)
            {
                _errors = e.Message;
                return null;
            }
            
        }

        private static string _errors;
        private static Type LoadViewModel(string viewModelName, string viewName)
        {
            CSharpCodeProvider provider = new CSharpCodeProvider();
            CompilerParameters parameters = new CompilerParameters { GenerateInMemory = true };
            parameters.ReferencedAssemblies.Add("System.dll");
            parameters.ReferencedAssemblies.Add(Assembly.GetEntryAssembly().Location);
            var code = File.ReadAllText(viewModelName);
            CompilerResults results = provider.CompileAssemblyFromSource(parameters, code);
            if (results.Errors.HasErrors)
            {
                _errors = "";
                foreach (CompilerError error in results.Errors)
                {
                    _errors += $"Error #{error.ErrorNumber}: {error.ErrorText}\n";
                }

                return null;
            }
            else
            {
                Assembly assembly = results.CompiledAssembly;
                return assembly.GetType($"DynamicVM.{viewName}");
            }
        }
    }
}

This code is very similar to the previous one, we’ve created a class with a static method Load, that loads a XAML file and, if there is a corresponding ViewModel, loads it and assigns it as the DataContext for the view. That way, the code in MainWindow.xaml.cs is simplified:

public MainWindow()
{
    InitializeComponent();
    var content = DynamicContentLoader.Load("Cust");
    if (DynamicContentLoader.Errors != null)
        MessageBox.Show(DynamicContentLoader.Errors, "Errors in compile");
    WndContent.Content = content;
}

Running the program, you will have the same result as the previous run.

Conclusions

As you’ve seen, if you want to have dynamic views, you are not limited to use codeless windows. You can also add code for it, compile it on the fly and use it tied to the view, thanks to the magic of data binding. That way, you can create applications fully dynamic, that can be easily updated whenever you want, with no need of recompilation and redeploy.

All the source code for this article is at https://github.com/bsonnino/DynamicXamlAndVM

After finishing my last post, I saw I could improve it a little bit. The UI was too raw (my goal at the time was not to show the UI, but show how to add logging to a UWP app) and could be improved: instead of listviews, why not use a DataGrid to show the client’s data?

Although there is no native DataGrid in the SDK, Telerik open sourced its UWP components, so you can use them in your UWP apps. If you want to know more,  you can go to the Telerik’s GitHub page (https://github.com/telerik/UI-For-UWP) and take a look at their components for UWP.

So, let’s start where we left: you can go to my GitHub page (https://github.com/bsonnino/LoggingSerilog) and download the project developed in the last post.  Then, add the NuGet package Telerik.UI.for.UniversalWindowsPlatform.

The first step in our change is to change the ListView in the Customers view to a RadDataGrid:

<grid:RadDataGrid ItemsSource="{Binding Customers}" SelectedItem="{Binding SelectedCustomer}"/>

You should add the namespace Telerik.UI.Xaml.Controls.Grid in the beginning pf the XAML file:

<UserControl
    x:Class="TelerikGrid.View.Customers"
    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"
    xmlns:grid="using:Telerik.UI.Xaml.Controls.Grid"
    mc:Ignorable="d"
    d:DesignHeight="500"
    d:DesignWidth="700"
    DataContext="{Binding Customers, Source={StaticResource Locator}}">

With this simple change, you get a full featured Data Grid, that allows sorting, filtering or grouping. You can group by any column by dragging it to the group box, at the left:

Really nice, no? If you take a look at the grid, you will see that all the columns are being shown, including InDesignMode, a property introduced in ViewModelBase, but we don’t want that. To set the columns, we have to set the property AutoGenerateColumns to False and set the columns we want in the Columns property. If you want, you can also set the CanUserChooseColumns to True, so the user can choose the columns he wants to display:

<grid:RadDataGrid ItemsSource="{Binding Customers}" SelectedItem="{Binding SelectedCustomer}" 
                  AutoGenerateColumns="False" CanUserChooseColumns="True">
    <grid:RadDataGrid.Columns>
        <grid:DataGridTextColumn PropertyName="Id"/>
        <grid:DataGridTextColumn PropertyName="Name"/>
        <grid:DataGridTextColumn PropertyName="City"/>
        <grid:DataGridTextColumn PropertyName="Country" />
    </grid:RadDataGrid.Columns>
</grid:RadDataGrid>

One extra twist is to add alternating columns to the grid. This is very easy, just set the AlternationStep property to a value greater than 1.

Now that we have the grid in place, let’s go to the second step: use a DataForm for the detail view, That way you can have a single control for easy editing of objects.

Adding a DataForm to edit the selected item

The DataForm is an easy way to edit objects in UWP. With it, you don’t need to add the editors for each field, you just need to add it and set the Item property to the item you want to edit:

<data:RadDataForm Item="{Binding SelectedCustomer}" />

As you can see, it works the same way as it did with all the TextBoxes, but the labels are not there. To fix this, we must add an attribute to the ViewModel properties to display the header:

public class CustomerViewModel : ViewModelBase
{
    private readonly Customer _customer;

    public CustomerViewModel(Customer customer)
    {
        _customer = customer;
    }

    [Display(Header = "Id", PlaceholderText = "Customer Id")]
    public string Id
    {
        get => _customer.Id;
        set
        {
            Log.Verbose("Customer Id changed from {OldId} to {NewId}", _customer.Id,value);
            _customer.Id = value;
            RaisePropertyChanged();
        }
    }

    [Display(Header = "Name", PlaceholderText = "Customer name")]
    public string Name
    {
        get => _customer.Name;
        set
        {
            Log.Verbose("Customer Name changed from {OldName} to {NewName}", _customer.Name, value);
            _customer.Name = value;
            RaisePropertyChanged();
        }
    }

    [Display(Header = "Address", PlaceholderText = "Customer address")]
    public string Address
    {
        get => _customer.Address;
        set
        {
            Log.Verbose("Customer Address changed from {OldAddress} to {NewAddress}", _customer.Address, value);
            _customer.Address = value;
            RaisePropertyChanged();
        }
    }

    [Display(Header = "City", PlaceholderText = "Customer city")]
    public string City
    {
        get => _customer.City;
        set
        {
            Log.Verbose("Customer City changed from {OldCity} to {NewCity}", _customer.City, value);
            _customer.City = value;
            RaisePropertyChanged();
        }
    }

    [Display(Header = "Country", PlaceholderText = "Customer country")]
    public string Country
    {
        get => _customer.Country;
        set
        {
            Log.Verbose("Customer Country changed from {OldCountry} to {NewCountry}", _customer.Country, value);
            _customer.Country = value;
            RaisePropertyChanged();
        }
    }

    [Display(Header = "Phone", PlaceholderText = "Customer phone")]
    public string Phone
    {
        get => _customer.Phone;
        set
        {
            Log.Verbose("Customer Phone changed from {OldPhone} to {NewPhone}", _customer.Phone, value);
            _customer.Phone = value;
            RaisePropertyChanged();
        }
    }
}

The Display attribute will tell the form what is the label that must be shown and the placeholder to show in the edit box when it’s empty. One note here is that the Display attribute that must be used isn’t in System.ComponentModel.DataAnnotations, but it is in Telerik.Data.Core. You must add the correct namespace to use the Header and PlaceHolderText properties. Once you make these changes, the labels appear in the form:

Adding a chart to the view

Now that our program is working like the old one with the Telerik controls, let’s enhance  it with a Pie Chart that shows the percentage of customers by country. To do that, we must create a new property in the CustomersViewModel,  CustomersByCountry. It will be initialized in the ViewModel’s constructor:

public CustomersViewModel()
{
    _selectedCustomer = _customers.Count > 0 ? _customers[0] : null;
    _newCustomerCommand = new RelayCommand(AddCustomer);
    _deleteCustomerCommand = new RelayCommand(DeleteCustomer, () => SelectedCustomer != null);
    CustomersByCountry = _customers.GroupBy(c => c.Country)
        .Select(g => new CustomerCountry(g.Key, g.Count()))
        .OrderByDescending(c => c.NumCustomers);
}

public IEnumerable<CustomerCountry> CustomersByCountry { get; }

We use LINQ to group the customers by country and generate the data ordered in descending order by the customer count. We have created a new class named CustomerCountry to store the total data for each country:

public class CustomerCountry
{
    public CustomerCountry(string country, int numCustomers)
    {
        Country = country;
        NumCustomers = numCustomers;
    }

    public string Country { get; }
    public int NumCustomers { get; }
}

Once we have that in place, we can create our chart view. In the View folder, create a new UserControl and name it CustomersChart. In the chart, add this code:

<UserControl
    x:Class="TelerikGrid.View.CustomersChart"
    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"
    xmlns:chart="using:Telerik.UI.Xaml.Controls.Chart"
    xmlns:primitives="using:Telerik.UI.Xaml.Controls.Primitives"
    mc:Ignorable="d"
    d:DesignHeight="300"
    d:DesignWidth="400"
    DataContext="{Binding Customers, Source={StaticResource Locator}}">

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" Margin="20">
        <chart:RadPieChart x:Name="Chart" PaletteName="DefaultLight" >
            <chart:PieSeries ItemsSource="{Binding CustomersByCountry}" ShowLabels="True" RadiusFactor="0.8">
                <chart:PieSeries.ValueBinding>
                    <chart:PropertyNameDataPointBinding PropertyName="NumCustomers" />
                </chart:PieSeries.ValueBinding>
                <chart:PieSeries.LegendTitleBinding>
                    <chart:PropertyNameDataPointBinding PropertyName="Country" />
                </chart:PieSeries.LegendTitleBinding>
            </chart:PieSeries>
        </chart:RadPieChart>
        <primitives:RadLegendControl LegendProvider="{Binding ElementName=Chart}">
            <primitives:RadLegendControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <ItemsWrapGrid Orientation="Vertical"/>
                </ItemsPanelTemplate>
            </primitives:RadLegendControl.ItemsPanel>
            <primitives:RadLegendControl.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal" Width="110">
                        <Ellipse Fill="{Binding Fill}" Stroke="{Binding Stroke}"
                                 StrokeThickness="1" Width="10" Height="10"/>
                        <TextBlock Text="{Binding Title}" Foreground="{Binding Fill}"
                                   Margin="10" />
                    </StackPanel>
                </DataTemplate>
            </primitives:RadLegendControl.ItemTemplate>
        </primitives:RadLegendControl>
    </Grid>
</UserControl>

We are adding a RadPieChart with a PieSeries in it. This PieSeries has the ItemsSource property set to the ViewModel’s CustomersByCountry property. Its values are set to the NumCustomers property and the Legends are set to the country names. To add a legend, we must add a RadLegendControl and set its LegendProvider property bound to the chart. The ItemsPanel property is set to a ItemsWrapGrid, in a way that the items span to a new column if there is no available space at the bottom. The labels of the legend have the same color of the pie.

Now, we must add the new view to the main view. I’ve chosen to replace the log view with this new view, In MainPage.xaml, put this code:

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <Grid.RowDefinitions>
        <RowDefinition Height="2*"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    <view:Customers />
    <!--<view:Log Grid.Row="1"/>-->
    <view:CustomersChart Grid.Row="1"/>
</Grid>

When you run the app, you will see something like this:

Note: this chart is not dynamic – if you change the country for a customer or add a new customer, the chart doesn’t update, because it’s calculated in the constructor, To make it dynamic, you should recalculate it when the country of a customer has changed.

With these changes, we have an improved application, with a lot of new features.

Conclusions

As you can see, with small changes we got a lot of improvements for our app. With the DataGrid we gor sorting, filtering and grouping for our data with almost no effort. With the DataForm we can edit the customer without having to add TexBlocks and TextBoxes. If we change the underlying class, the form will be updated automatically. With the chart we could show a new visualization for our data. The Telerik controls for UWP are open source and free for you to use.

All the source code in this article is in https://github.com/bsonnino/TelerikGrid

In the last post, I showed how to load a complete view dynamically, but that’s not the most common way to change the visualization of data. Most of the time, you will want to just change the appearance of the view, customizing the style for a client.

If we want to apply the styles for all controls, we must use a Theme. A Theme provides the default appearance for all controls of an application. The default theme is provided by the OS the app is running, but that can be changed by using an alternate theme file, a resource dictionary that has default styles for all controls.

Loading a theme dynamically

One way to load themes dynamically is to use any theme dll available for our use (many of them are free). We can add the themes to our project using Nuget and then change the theme as we want, but that’s not the focus, here. We want to load the themes from loose XAML files dynamically.

For that, I used an open source project, named WpfThemes (https://wpfthemes.codeplex.com/), that has several themes available and creates a themes dll that can be used in our project. As it is open source, I won’t use the dll, but I will use the theme structure and its demo to show how to load the theme from its files.

I downloaded the project and restructured it in this way:

  • I created a single project, LoadThemes, and added the content of the demo main window to the new project’s main window
  • I created a new folder on the project, named Themes and added all themes folders from the WPF.Themes project
  • I set all Theme.xaml file Build Action to None and Copy to output Directory to “ Copy if newer”

With this structure, we can load all available theme names to the theme combobox using this code:

[sourcecode language='csharp'  padlinenumbers='true']
private void LoadThemes()
{
    themes.ItemsSource = Directory.EnumerateDirectories("Themes").Select(t => t.Substring(7));
}
[/sourcecode]

I just enumerate all folders under the Themes folder and get the folder names, removing the initial “Themes\”. Now, we must load the theme dynamically. This is done in the SelectionChanged event for the theme combobox:

[sourcecode language='csharp'  padlinenumbers='true']
private void themes_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
{
    if (e.AddedItems.Count == 0)
        return;
    using (FileStream fs = new FileStream($"Themes\\{e.AddedItems[0]}\\Theme.xaml", FileMode.Open))
    {
        var dict = XamlReader.Load(fs) as ResourceDictionary;
        if (dict != null)
        {
            Application.Current.Resources.Clear();
            Application.Current.Resources.MergedDictionaries.Add(dict);
        }
    }
}
[/sourcecode]

The program gets the selected item and opens the Theme.xaml located in the selected folder. Then, it loads the file as a ResourceDictionary and sets the MergedDictionaries for the current Application to it. This is everything that’s needed to change the style of all controls at once.

image

Loading styles dynamically

Usually, you do not want such a radical change, like the theme change, but you want to change the way some controls are drawn (colors, margins, etc). For that, you just need to change some styles. One way to do it is to apply some styles to your controls and then load the resource dictionaries dynamically. For example, the project from previous post could be redesigned by using styles. We can take the Master/Detail file and apply styles to the file, setting something like this:

[sourcecode language='xml' ]
<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" Foreground="{StaticResource PageForeground}">
    <UserControl.Resources>
        <Style TargetType="TextBlock" >
            <Setter Property="Foreground" Value="{StaticResource PageForeground}" />
        </Style>
    </UserControl.Resources>
    <Grid Background="{StaticResource PageBackground}">
        <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}" Style="{StaticResource ButtonStyle}"/>
        </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>
                <TextBlock Text="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"  />
                <TextBlock Text="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"  />
                <TextBlock Text="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"  />
                <TextBlock Text="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"  />
                <TextBlock Text="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" />
                <TextBlock Text="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"  />
                <TextBlock Text="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"  />
                <TextBlock Text="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"  />
                <TextBlock Text="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"  />
                <TextBlock Text="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"  />
                <TextBlock Text="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}" Style="{StaticResource ButtonStyle}"/>
            <Button Width="75" Height="25" Margin="5" Content="Remove" Command="{Binding RemoveCommand}" Style="{StaticResource ButtonStyle}"/>
            <Button Width="75" Height="25" Margin="5" Content="Save" Command="{Binding SaveCommand}" Style="{StaticResource ButtonStyle}"/>
        </StackPanel>
    </Grid>
</UserControl>
[/sourcecode]

We can create a default resource dictionary and call it DefaultDict.xaml:

[sourcecode language='xml' ]
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:local="clr-namespace:CustomerApp">
    <SolidColorBrush x:Key="PageForeground" Color="Black" />
    <SolidColorBrush x:Key="PageBackground" Color="AliceBlue" />
    <Style x:Key="ButtonStyle" TargetType="Button">
        <Setter Property="Background" Value="BlanchedAlmond" />
        <Setter Property="Margin" Value="10,0" />
    </Style>
</ResourceDictionary>
[/sourcecode]

And add this dictionary to App.xaml:

[sourcecode language='xml' ]
<Application.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary Source="DefaultDict.xaml" />
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</Application.Resources>
[/sourcecode]

When we run the application, the default style is applied to the view:

image

Now, we want to apply dynamic styles to the view, so we can create loose files with new resource dictionaries. In the project, create a new folder and name it Styles. In this folders, add three files and name them as Blue.xaml, Red.xaml and Yellow.xaml:

[sourcecode language='xml' ]
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:local="clr-namespace:CustomerApp">
    <SolidColorBrush x:Key="PageForeground" Color="White" />
    <SolidColorBrush x:Key="PageBackground" Color="Navy" />
    <Style x:Key="ButtonStyle" TargetType="Button">
        <Setter Property="Background" Value="Azure" />
        <Setter Property="Margin" Value="10,0" />
    </Style>
    <Style TargetType="TextBlock" >
        <Setter Property="FontFamily" Value="Arial" />
    </Style>
</ResourceDictionary>
[/sourcecode]
[sourcecode language='xml' ]
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:local="clr-namespace:CustomerApp">
    <SolidColorBrush x:Key="PageForeground" Color="White" />
    <SolidColorBrush x:Key="PageBackground" Color="Red" />
    <Style x:Key="ButtonStyle" TargetType="Button">
        <Setter Property="Background" Value="LightPink" />
        <Setter Property="Margin" Value="10,0" />
    </Style>
    <Style TargetType="TextBlock" >
        <Setter Property="FontFamily" Value="Times New Roman" />
    </Style>
</ResourceDictionary>
[/sourcecode]
[sourcecode language='xml' ]
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:local="clr-namespace:CustomerApp">
    <SolidColorBrush x:Key="PageForeground" Color="Black" />
    <SolidColorBrush x:Key="PageBackground" Color="Yellow" />
    <Style x:Key="ButtonStyle" TargetType="Button">
        <Setter Property="Background" Value="BlanchedAlmond" />
        <Setter Property="Margin" Value="10,0" />
    </Style>
    <Style TargetType="TextBlock" >
        <Setter Property="FontFamily" Value="Comic Sans MS" />
    </Style>
</ResourceDictionary>
[/sourcecode]

And set their Build Action to None and Copy to output directory to “Copy if newer”. As you can see, these files are very similar to the default dictionary: they override some colors and styles and add a new one, setting the font for all TextBlocks.

Then, add a new Combobox in the main window, to select the new styles:

[sourcecode language='xml' ]
<ComboBox x:Name="StylesCombo" Height="30" Width="200" 
            SelectionChanged="SelectedStyleChanged" HorizontalAlignment="Right" Margin="5" />
[/sourcecode]

The SelectionChanged event handler for this box is:

[sourcecode language='csharp' ]
private void SelectedStyleChanged(object sender, SelectionChangedEventArgs e)
{
    if (e.AddedItems.Count == 0)
            return;
        using (FileStream fs = new FileStream($"Styles\\{e.AddedItems[0]}", FileMode.Open))
        {
            var dict = XamlReader.Load(fs) as ResourceDictionary;
            if (dict != null)
            {
                Application.Current.Resources.MergedDictionaries.Add(dict);
            }
        }
}
[/sourcecode]

When you run the program and try to change the styles, you will see a strange thing:

image

The font that was not defined in the default style is changed, but the colors have not changed. This happens because we have set the styles as Static, using StaticResource when we applied the styles. In order for the style be applied dynamically, we must apply it using DynamicResource. When we change all StaticResource to DynamicResource in the master/detail file, we get this:

image

All the colors and styles have changed.

Conclusions

As you can see, WPF gives you a great flexibility to change you views dynamically. You can change the entire view, restyle all controls or only change the style of part of the controls. Everything can be done at runtime, and you can load the files from anywhere you want: loose files in a selected folder, from a database or from a request to a server, you can load the XAML as a string, parse and load it into your program.

The files for this project are in https://github.com/bsonnino/LoadThemes (runtime themes) and https://github.com/bsonnino/DynamicXamlViews (dynamic styles).

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:

clip_image002

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