Introduction

You have an old, legacy app (with no tests), and its age is starting to show – it’s using an old version of the .NET Framework, it’s difficult to maintain and every new feature introduced brings a lot of bugs. Developers are afraid to change it, but the users ask for new features. You are at a crossroad: throw the code and rewrite everything or refactor the code. Does this sound familiar to you ?

I’m almost sure that you’re leaning to throw the code and rewrite everything. Start fresh, use new technologies and create the wonderful app you’ve always dreamed of. But this comes with a cost: the old app is still there, functional, and must be maintained while you are developing the new one. There are no resources to develop both apps in parallel and the new app will take a long time before its finished.

So, the only way to go is to refactor the old app. It’s not what you wanted, but it can still be fun – you will be able to use the new technologies, introduce good programming practices, and at the end, have the app you have dreamed. No, I’m not saying it will be an easy way, but it will be the most viable one.

This article will show how to port a .NET 4 WPF app and port it to .NET 5, introduce the MVVM pattern and add tests to it. After that, you will be able to change its UI, using WinUI3, like we did in this article.

The original app

The original app is a Customer CRUD, developed in .NET 4, with two projects – the UI project, CustomerApp, and a library CustomerLib, that access client’s data in an XML file (I did that just for the sake of simplicity, but this could be changed easily for another data source, like a database). You can get the app from here, and when you run it, you get something like this:

The first step will be converting it to .NET 5. Before that, we will see how portable is our app, using the .NET Portability analyzer. It’s a Visual studio extension that you can download from here. Once you download and install it, ou can run it in Visual Studio with Analyze/Portability Analyzer Settings:

You must select the platforms you want and click OK. Then, you must select Analyze/Analyze Assembly Portability, select the executables for the app and click OK. That will generate an Excel file with the report:

As you can see, our app can be ported safely to .NET 5. If there are any problems, you can check them in the Details tab. There, you will have a list of all APIs that you won’t be able to port, where you should find a workaround. Now, we’ll start converting the app.

Converting the app to .NET 5

To convert the app, we’ll start converting the lib to .NET Standard 2.0. To do that, right-click in the Lib project in the Solution Explorer and  select Unload Project, then edit the project file and change it to:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFrameworks>netstandard2.0</TargetFrameworks>
  </PropertyGroup>
  <ItemGroup>
    <Content Include="Customers.xml">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </Content>
  </ItemGroup>
</Project>

The project file is very simple, just set the taget framework to netstandard2.0 and copy the item group relative to the xml file, so it’s included in the final project. Then, you must reload the project and remove the AssemblyInfo  file from the Properties folder, as it isn’t needed anymore (if you leave it, it will generate an error, as it’s included automatically by the new project file).

Then, right click the app project in the Solution Explorer and select Unload Project, to edit the project file:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net5.0-windows</TargetFramework>
    <UseWPF>true</UseWPF>
    <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
  </PropertyGroup>
  <ItemGroup>
    <ProjectReference Include="..\CustomerLib\CustomerLib.csproj">
      <Name>CustomerLib</Name>
    </ProjectReference>
  </ItemGroup>
</Project>

We are setting the output type to WinExe, setting the target framework to net5.0-windows, telling that we will use the .net 5 features, plus the ones specific to Windows. If we don’t do that, we wouldn’t be able to use the WPF features, that are specific to Windows and set UseWPF to true. Then, we copy the lib’s ItemGroup to the project. When we reload the project, we must delete the Properties folder and we can build our app, converted to .NET 5. It should work exactly the way it did before. We are in the good path, porting the app to the newest .NET version, but we still have a long way to go. Now it’s time to add good practices to our app, using the MVVM Pattern.

The MVVM Pattern

The MVVM (Model-View-ViewModel) pattern was created on 2005 by John Gossman, a Microsoft Architect on Blend team, and it makes extensive use of the DataBinding feature existent in WPF and other XAML platforms (like UWP or Xamarin). It provides separation between data (Model) and its visualization (View), using a binding layer, the ViewModel.

The  ViewModel  is a class that implements the INotifyPropertyChanged interface:

public interface INotifyPropertyChanged 
{ 
  event PropertyChangedEventHandler PropertyChanged; 
}

It has just one event, PropertyChanged that is activated when there is a change in a property. The Data binding mechanism present in WPF (and in other XAML platforms) subscribes this event and updates the view with no program intervention. So, all we need to do is to create a class that implements INotifyPropertyChanged and call this event when there is a change in a property to WPF update the view.

The greatest advantage is that the ViewModel is a normal class and doesn’t have any dependency on the view layer. That way, we don’t need to initialize a window when we test the ViewModel. This image shows the basic structure of this pattern:

The model communicates with the ViewModel by its properties and methods. The ViewModel communicates with the View mainly using Data Binding, it receives Commands from the View and can send messages to it. When there are many ViewModels that must communicate , they usually send messages, to maintain a decoupled architecture. That way, the Model (usually a POCO class – Plain Old CSharp Object) doesn’t know about the ViewModel, the ViewModel isn’t coupled with the View or other ViewModels, and the View isn’t tied to a ViewModel directly (the only tie is the View’s DataContext property, that will bind the View and the ViewModel. The rest will be done by Data Binding).

We could implement all this infrastructure by ourselves, it’s not a difficult task, but it’s better to use a Framework for that. There are many MVVM frameworks out there, each one chooses a different approach to implement the infrastructure and select one of them is just a matter of preference. I’ve used MVVM Light toolkit for years, it’s very lightweight and easy to use, but it isn’t maintained anymore, so I decided to search another framework. Fortunately, the Windows Community Toolkit has provided a new framework, inspired on MVVM Light, the MVVM Community Toolkit.

Implementing the MVVM Pattern

In our project, the Model is already separated from the rest: it’s in the Lib project and we’ll leave that untouched, as we don’t have to change anything in the model. In order to use the MVVM Toolkit, we must add the Microsoft.Toolkit.Mvvm NuGet package.

Then, we must create the ViewModels to interact between the View and the Model. Create a new folder and name it ViewModel. In it, add a new class and name it MainViewModel.cs.  This class will inherit from ObservableObject, from the toolkit, as it already implements the interface. Then we will copy and adapt the code that is found in the code behind of MainWindow.xaml.cs:

 

public class MainViewModel : ObservableObject
{
    private readonly ICustomerRepository _customerRepository;
    private Customer _selectedCustomer;

    public MainViewModel()
    {
        _customerRepository =  new CustomerRepository();
        AddCommand = new RelayCommand(DoAdd);
        RemoveCommand = new RelayCommand(DoRemove, () => SelectedCustomer != null);
        SaveCommand = new RelayCommand(DoSave);
        SearchCommand = new RelayCommand<string>(DoSearch);
    }

    public IEnumerable<Customer> Customers => _customerRepository.Customers;

    public Customer SelectedCustomer
    {
        get => _selectedCustomer;
        set 
        { 
            SetProperty(ref _selectedCustomer, value); 
            RemoveCommand.NotifyCanExecuteChanged(); 
        } 
    }

    public IRelayCommand AddCommand { get; }
    public IRelayCommand RemoveCommand { get; }
    public IRelayCommand SaveCommand { get; }
    public IRelayCommand<string> SearchCommand { get; }
    private void DoAdd()
    {
        var customer = new Customer();
        _customerRepository.Add(customer);
        SelectedCustomer = customer;
        OnPropertyChanged("Customers");
    }

    private void DoRemove()
    {
        if (SelectedCustomer != null)
        {
            _customerRepository.Remove(SelectedCustomer);
            SelectedCustomer = null;
            OnPropertyChanged("Customers");
        }
    }

    private void DoSave()
    {
        _customerRepository.Commit();
    }

    private void DoSearch(string textToSearch)
    {
        var coll = CollectionViewSource.GetDefaultView(Customers);
        if (!string.IsNullOrWhiteSpace(textToSearch))
            coll.Filter = c => ((Customer)c).Country.ToLower().Contains(textToSearch.ToLower());
        else
            coll.Filter = null;
    }
}

We have two properties, Customers and SelectedCustomerCustomers  will contain the list of customers shown in the DataGrid. SelectedCustomer will be the selected customer in the DataGrid, that will be shown in the detail pane. There are four commands, and we will use the IRelayCommand interface, declared in the toolkit. Each command will be initialized with the method that will be executed when the command is invoked. The RemoveCommand uses an overload for the constructor, that uses a predicate as the second parameter. This predicate will only enable the button when there is a customer selected in the DataGrid. As this command is dependent on the selected customer, when we change this property, we call the NotifyCanExecuteChanged method to notify all the elements that are bound to this command.

Now we can remove all the code from MainWindow.xaml.cs and leave only this:

public MainWindow()
{
    InitializeComponent();
    DataContext = new MainViewModel();
}

We can run the program and see that it runs the same way it did before, but we made a large refactoring to the code and now we can start implementing unit tests in the code.

Implementing tests

Now that we’ve separated the code from the view, we can test the ViewModel without the need to initialize a Window. That is really great, because we can have testable code and be assured that we are not breaking anything when we are implementing new features. For that, add a new test project and name it CustomerApp.Tests. In the Visual Studio version I’m using, there is no template for the .net 5.0 test project available, so I added a .Net Core test project, then I edited the project file and changed the TargetFramework to net5.0-windows. Then, you can add a reference to the CustomerApp project and rename UnitTest1 to MainViewModelTests.

Taking a look at the Main ViewModel, we see that there is a coupling between it and the Customer Repository. In this case, there is no much trouble, because we are reading the customers from a XML file located in the output directory, but if we decide to replace it with some kind of database, it can be tricky to test the ViewModel, because we would have to do a lot of setup to test it.

We’ll remove the dependency using Dependency Injection. Instead of using another framework for the the dependency injection, we’ll use the integrated one, based on Microsoft.Extensions.DependencyInjection. You should add this NuGet package in the App project to use the dependency injection. Then, in App.xaml.cs, we’ll add code to initialize the location of the services:

public partial class App
{
    public App()
    {
        Services = ConfigureServices();
    }

    public new static App Current => (App) Application.Current;

    public IServiceProvider Services { get; }

    private static IServiceProvider ConfigureServices()
    {
        var services = new ServiceCollection();

        services.AddSingleton<ICustomerRepository, CustomerRepository>();
        services.AddSingleton<MainViewModel>();

        return services.BuildServiceProvider();
    }

    public MainViewModel MainVM => Services.GetService<MainViewModel>();
}

We declare a static property Current to ease using the App  object and declare a IServiceProvider, to provide our services. They are configured in the ConfigureServices method, that creates a ServiceCollection and add the CustomerRepository and the main ViewModel to the collection. ConfigureServices  is called in the constructor of the application. Finally we declare the property MainVM, which will get the ViewModel from the Service Collection.

Now, we can change MainWindow.xaml.cs to use the property instead of instantiate directly the ViewModel:

public MainWindow()
{
    InitializeComponent();
    DataContext = App.Current.MainVM;
}

The last change is to remove the coupling between the ViewModel and the repository using Dependency Injection, in MainViewModel.cs:

public MainViewModel(ICustomerRepository customerRepository)
{
    _customerRepository = customerRepository ??  
                          throw new ArgumentNullException("customerRepository");
    _customerRepository = customerRepository;
    AddCommand = new RelayCommand(DoAdd);
    RemoveCommand = new RelayCommand(DoRemove, () => SelectedCustomer != null);
    SaveCommand = new RelayCommand(DoSave);
    SearchCommand = new RelayCommand<string>(DoSearch);
}

With that, we’ve gone one step further and removed the coupling between the ViewModel and the repository, so we can start our tests.

For the tests, we will use two libraries, FluentAssertions, for better assertions and FakeItEasy, to generate fakes. You should install both NuGet packages to your test project. Now, we can start creating our tests:

public class MainViewModelTests
{
    [TestMethod]
    public void Constructor_NullRepository_ShouldThrow()
    {
        Action act = () => new MainViewModel(null);

        act.Should().Throw<ArgumentNullException>()
            .Where(e => e.Message.Contains("customerRepository"));
    }

    [TestMethod]
    public void Constructor_Customers_ShouldHaveValue()
    {
        var repository = A.Fake<ICustomerRepository>();
        var customers = new List<Customer>();
        A.CallTo(() => repository.Customers).Returns(customers);
        var vm =  new MainViewModel(repository);

        vm.Customers.Should().BeEquivalentTo(customers);
    }

    [TestMethod]
    public void Constructor_SelectedCustomer_ShouldBeNull()
    {
        var repository = A.Fake<ICustomerRepository>();
        var vm = new MainViewModel(repository);

        vm.SelectedCustomer.Should().BeNull();
    }
}

Here we created three tests for the constructor, testing the values of the properties after the constructor. We can continue, testing the commands in the ViewModel:

[TestMethod]
public void AddCommand_ShouldAddInRepository()
{
    var repository = A.Fake<ICustomerRepository>();
    var vm = new MainViewModel(repository);

    vm.AddCommand.Execute(null);
    A.CallTo(() => repository.Add(A<Customer>._)).MustHaveHappened();
}

[TestMethod]
public void AddCommand_SelectedCustomer_ShouldNotBeNull()
{
    var repository = A.Fake<ICustomerRepository>();
    var vm = new MainViewModel(repository);
    vm.AddCommand.Execute(null);
    vm.SelectedCustomer.Should().NotBeNull();
}

[TestMethod]
public void AddCommand_ShouldNotifyCustomers()
{
    var repository = A.Fake<ICustomerRepository>();
    var vm = new MainViewModel(repository);
    var wasNotified = false;
    vm.PropertyChanged += (s, e) =>
    {
        if (e.PropertyName == "Customers")
            wasNotified = true;
    };
    vm.AddCommand.Execute(null);
    wasNotified.Should().BeTrue();
}

[TestMethod]
public void RemoveCommand_SelectedCustomerNull_ShouldNotRemoveInRepository()
{
    var repository = A.Fake<ICustomerRepository>();
    var vm = new MainViewModel(repository);
    vm.RemoveCommand.Execute(null);
    A.CallTo(() => repository.Remove(A<Customer>._)).MustNotHaveHappened();
}

[TestMethod]
public void RemoveCommand_SelectedCustomerNotNull_ShouldRemoveInRepository()
{
    var repository = A.Fake<ICustomerRepository>();
    var vm = new MainViewModel(repository);
    vm.SelectedCustomer = new Customer();
    vm.RemoveCommand.Execute(null);
    A.CallTo(() => repository.Remove(A<Customer>._)).MustHaveHappened();
}

[TestMethod]
public void RemoveCommand_SelectedCustomer_ShouldBeNull()
{
    var repository = A.Fake<ICustomerRepository>();
    var vm = new MainViewModel(repository);
    vm.SelectedCustomer = new Customer();
    vm.RemoveCommand.Execute(null);
    vm.SelectedCustomer.Should().BeNull();
}

[TestMethod]
public void RemoveCommand_ShouldNotifyCustomers()
{
    var repository = A.Fake<ICustomerRepository>();
    var vm = new MainViewModel(repository);
    vm.SelectedCustomer = new Customer(); 
    var wasNotified = false;
    vm.PropertyChanged += (s, e) =>
    {
        if (e.PropertyName == "Customers")
            wasNotified = true;
    };
    vm.RemoveCommand.Execute(null);
    wasNotified.Should().BeTrue();
}

[TestMethod]
public void SaveCommand_ShouldCommitInRepository()
{
    var repository = A.Fake<ICustomerRepository>();
    var vm = new MainViewModel(repository);
    vm.SaveCommand.Execute(null);
    A.CallTo(() => repository.Commit()).MustHaveHappened();
}

[TestMethod]
public void SearchCommand_WithText_ShouldSetFilter()
{
    var repository = A.Fake<ICustomerRepository>();
    var vm = new MainViewModel(repository);
    vm.SearchCommand.Execute("text");
    var coll = CollectionViewSource.GetDefaultView(vm.Customers);
    coll.Filter.Should().NotBeNull();
}

[TestMethod]
public void SearchCommand_WithoutText_ShouldSetFilter()
{
    var repository = A.Fake<ICustomerRepository>();
    var vm = new MainViewModel(repository);
    vm.SearchCommand.Execute("");
    var coll = CollectionViewSource.GetDefaultView(vm.Customers);
    coll.Filter.Should().BeNull();
}

Now we have all the tests for the ViewModel and have our project ready for the future. We went step by step and finished with a .NET 5.0 project that uses the MVVM pattern and have unit tests. This project is ready to be updated to WinUI3, or even to be ported to UWP or Xamarin. The separation between the code and the UI makes it easy to port it to other platforms, the ViewModel became testable and you can test all logic in it, without bothering with the UI. Nice, no ?

The full source code for the project is at https://github.com/bsonnino/MvvmApp

 

You have an app developed a long time ago and it’s showing its age. It’s time to modernize it, but rewrite isn’t an option: it’s too complicated to rewrite it and it’s still working fine, there is no budget for rewriting the app, there are other priorities, and so on.

If the main reason for not rewriting the app is that it’s working fine, and the only thing that is showing its age is the UI, you have no reason to not modernize it. Microsoft sent the developers a clear message that WPF, Winforms and Win32 are alive and well, open sourcing them and porting to .NET Core. And, the best thing is that you can use the newest features in the Operating System and integrate them to your app without the need to rewrite it. You can even use the new UI of UWP apps in your own app, by using the technology named XamlIslands, where you can embed your own controls in your old app.

To show how this is done, we’ll create a WPF app that shows images and modernize it with Xaml Islands. For this app, I’ve dowloaded 30 images from http://unsample.net/ . This service sends me a zip file with a maximum of 30 photos, downloaded from https://unsplash.com, with a Json file with the credits. Our apps will show the photos and the credits.

Initially, go to http://unsample.net/ and download a set of 30 photos. In Visual Studio, create a new WPF app and name it WPFXamlIslands. In the Solution Explorer, create a new folder named Photos in the project and add the photos and the Json file from the zip to it. Select all files in the folder and change the Build Action to None and the Copy to Output Directory to Copy if Newer.

As we will be manipulating Json files, right-click on the References node and select Manage NuGet Packages, then install the Newtonsoft.Json package. After that, install the MVVM Light package, as we will be using MVVM for this project. You will have to remove the Microsoft.Practices.ServiceLocation using clause in the ViewModelLocator class and add the CommonServiceLocator using clause in the same file to make it compile.

Then, in MainWindow.xaml file, add this code:

<Window x:Class="WPFXamlIslands.MainWindow"
        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:c="clr-namespace:WPFXamlIslands.Converters"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800"
        DataContext="{Binding Source={StaticResource Locator}, Path=Main}">
    <Window.Resources>
        <c:StringToImageConverter x:Key="StringToImageConverter" />
    </Window.Resources>
    <Grid>
        <ScrollViewer HorizontalScrollBarVisibility="Disabled">
            <ItemsControl ItemsSource="{Binding Photos}" >
                <ItemsControl.ItemsPanel>
                    <ItemsPanelTemplate>
                        <WrapPanel />
                    </ItemsPanelTemplate>
                </ItemsControl.ItemsPanel>
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <Border BorderBrush="Black" Background="Beige"  BorderThickness="1" Margin="5">
                            <StackPanel Margin="5">
                                <Image Source="{Binding Key, Converter={StaticResource StringToImageConverter}}" 
                                       Width="150" Height="150" Stretch="Uniform" />
                                <TextBlock Text="{Binding Value.UserName}" MaxWidth="150" Margin="0,5" />
                            </StackPanel>
                        </Border>
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>
        </ScrollViewer>
    </Grid>
</Window>

We’ve added a ItemsControl with a datatemplate to show the images and the name of the author. The items are presented in a WrapGrid, so the items are wrapped and the number of items change depending on the window width. To present the images, I’ve created a converter to convert the name of the image to a bitmap that can be assigned to the image:

public class StringToImageConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        string imagePath = $"{AppDomain.CurrentDomain.BaseDirectory}Photos\\{value}.jpg";
        BitmapImage bitmapImage = !string.IsNullOrWhiteSpace(value?.ToString()) &&
            File.Exists(imagePath) ?
            new BitmapImage(new Uri(imagePath)) :
            null;
        return bitmapImage;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

It will take the name of the image and create a BitmapImage with it. That way, we can use the converter in the data binding for the list items. The MainViewModel will be like this:

public class MainViewModel : ViewModelBase
{
    /// <summary>
    /// Initializes a new instance of the MainViewModel class.
    /// </summary>
    public MainViewModel()
    {
        Photos = JsonConvert.DeserializeObject<Dictionary<string, PhotoData>>(
            File.ReadAllText("Photos\\__credits.json"),
            new JsonSerializerSettings
            {
                ContractResolver = new DefaultContractResolver
                {
                    NamingStrategy = new SnakeCaseNamingStrategy()
                }
            });
    }

    public Dictionary<string, PhotoData> Photos { get; private set; }

It will read the files, deserialize the Json file and assign the resulting Dictionary to the property Photos. This dictionary has the name of the file as the key and a class named PhotoData as the value. PhotoData is declared as:

public class PhotoData
{
    public string UserName { get; set; }
    public string UserUrl { get; set; }
    public string PhotoUrl { get; set; }
}

Now, when you run the application, it will show something like this:

The app runs fine, but it can be improved to add the new features and animations given by UWP, using the Xaml Islands.

The easiest way to use a UWP control in a WPF or Winforms app is to use the Windows Community Toolkit. This is a toolkit of components created by the community and Microsoft and can be found on https://github.com/windows-toolkit/WindowsCommunityToolkit.

To use a UWP control, you must use the WindowsXamlHost  control in the window. It can be found in the Microsoft.Toolkit.WPF.UI.XamlHost  NuGet package. Install it and add a WindowsXamlHost control in the main window:

<Grid>
    <xaml:WindowsXamlHost x:Name="UwpButton" 
                          InitialTypeName="Windows.UI.Xaml.Controls.Button"
                          ChildChanged="UwpButton_ChildChanged" />
</Grid>

In the code behind, you must add the code to initialize the button in the event handler for ChildChanged:

private void UwpButton_ChildChanged(object sender, EventArgs e)
{
    WindowsXamlHost windowsXamlHost = (WindowsXamlHost)sender;

    Windows.UI.Xaml.Controls.Button button =
        (Windows.UI.Xaml.Controls.Button)windowsXamlHost.Child;
    if (button == null)
        return;
    button.Width = 100;
    button.Height = 40;
    button.Content = "UWP button";
    button.Click += Button_Click;
}

private void Button_Click(object sender, Windows.UI.Xaml.RoutedEventArgs e)
{
    MessageBox.Show("UWP button works");
}

The ChildChanged is called when the child in the XamlHost changes. There you must configure the control added as a child (with the use of the InitialTypeName property).

That should be everything, but when you see the code, you see that the Button is not defined. In the error window, there is a warning saying that that Windows.Foundation.UniversalApiContract is missing. My first try was to find a dll with this name, which couldn’t be found. Then I noticed that what was needed was not the dll, but a winmd file with the Windows Metadata for the controls. In fact, there is a Windows.Foundation.UniversalApiContract.winmd file located in C:\Program Files (x86)\Windows Kits\10\References\10.0.18362.0\Windows.Foundation.UniversalApiContract\8.0.0.0\ (the version in your system might change), and I added this file as a reference and the errors regarding the button disappeared.

Then I ran the project and got a Catatstrophic Failure (this one is nice – I was expecting my computer to melt down, but fortunately, that didn’t occur :-)). After some more research, I came to this article (yes, Microsoft also suffers with Catastrophic Failures :-)), and the answer was pretty simple: add an Application Manifest. In the Solution Explorer, add an Application Manifest and change it like this:

<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
  <application>
    <!-- A list of the Windows versions that this application has been tested on
         and is designed to work with. Uncomment the appropriate elements
         and Windows will automatically select the most compatible environment. -->

    <!-- Windows Vista -->
    <!--<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />-->

    <!-- Windows 7 -->
    <!--<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />-->

    <!-- Windows 8 -->
    <!--<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />-->

    <!-- Windows 8.1 -->
    <!--<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />-->

    <!-- Windows 10 -->
    <maxversiontested Id="10.0.18358.0"/>0
    <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />

  </application>
</compatibility>

That will set the MaxVersionTested and the error disappears. When you run the application, you will have something like this:

Now we can see that our program works with the UWP control, then let’s continue to modernize it. We will add a FlipView to show the images. For that, we must change the InitialTypeName of the WindowsXamlHost to show the FlipView:

<xaml:WindowsXamlHost x:Name="XamlHost" 
                      InitialTypeName="Windows.UI.Xaml.Controls.FlipView" 
                      ChildChanged="XamlHost_ChildChanged" />

The code for the ChildChanged event will configure the FlipView and its DataTemplate:

        private void XamlHost_ChildChanged(object sender, EventArgs e)
        {
            WindowsXamlHost windowsXamlHost = (WindowsXamlHost)sender;

            Windows.UI.Xaml.Controls.FlipView flipView =
                (Windows.UI.Xaml.Controls.FlipView)windowsXamlHost.Child;
            if (flipView == null)
                return;
            var dataTemplate = (Windows.UI.Xaml.DataTemplate)XamlReader.Load(@"
<DataTemplate xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation"">
  <Grid Margin=""5"">
      <Grid.RowDefinitions>
         <RowDefinition Height=""*"" />
         <RowDefinition Height=""40"" />
      </Grid.RowDefinitions>
      <Image Source=""{Binding PhotoUrl}"" Grid.Row=""0"" Margin=""5""
            Stretch=""Uniform"" />
      <TextBlock Text=""{Binding UserName}"" HorizontalAlignment=""Center""
            VerticalAlignment=""Center"" Grid.Row=""1""/>
  </Grid>
</DataTemplate>");
            
            flipView.ItemTemplate = dataTemplate;
            flipView.ItemsSource = ((MainViewModel)DataContext).Photos;
        }

We create the DataTemplate as a string and load it with XamlReader.Read, then set the ItemsSource to the Photos property of the ViewModel. In order to use it in a UWP control, we modified the obtention of the Photos property:

public class MainViewModel : ViewModelBase
{
    /// <summary>
    /// Initializes a new instance of the MainViewModel class.
    /// </summary>
    public MainViewModel()
    {
        Photos = JsonConvert.DeserializeObject<Dictionary<string, PhotoData>>(
            File.ReadAllText("Photos\\__credits.json"),
            new JsonSerializerSettings
            {
                ContractResolver = new DefaultContractResolver
                {
                    NamingStrategy = new SnakeCaseNamingStrategy()
                }
            }).Select(p => new PhotoData() {PhotoUrl = $".\\Photos\\{p.Key}.jpg", UserName = p.Value.UserName});
    }

    public IEnumerable<PhotoData> Photos { get; private set; }

}

With these changes, you can now run the program and see the photos in a FlipView:

Conclusion

As you can see, you can modernize your desktop application with UWP controls, using Xaml Islands. The WindowsXamlHost eases this work a lot, but the work is still clumsy: you must add the winmd file, add the manifest to the project and manipulate the UWP control in code, using the Windows.UI.Xaml namespace. Adding a DataTemplate to the FlipView requires parsing Xaml code that comes from a string. Not a simple task, but still feasible. Hopefully, things will be easier with Project Reunion and WinUI 3.

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