In-Depth Technical Analysis of XAML-Based Frameworks and Cross-Platform Project Architecture Design
This article is a restructured version of the presentation given at the BMW Meetup session held at Microsoft's headquarters in Korea on October 22nd. In this seminar, we delve deeply into various XAML-based platforms, cross-platform strategies, and the core technologies essential for effective project architecture design.
Introduction
Hello, I'm Jaewung Lee, a Microsoft MVP (Windows Development). Starting with WPF, I have a deep interest and experience in XAML-based frameworks and project architecture design, including the Uno Platform. You can find various projects I've worked on at my GitHub:
Table of Contents
Overview of XAML-Based Platforms and Cross-Platform Development
.NET Version Selection Strategy for Cross-Platform Considerations
Analysis of View and ViewModel Connection Strategies
Essential Features and Implementation Plans for Framework Design
Key Strategies for Effectively Using WPF Technology on Other Platforms
Bootstrapper Design Methodology for Distributed Project Management
Strategies to Maximize WPF Technology in Desktop Cross-Platform Environments
1. Overview of XAML-Based Platforms and Cross-Platform Development
XAML (eXtensible Application Markup Language) is a markup language used to declaratively define UIs and is utilized across various platforms. XAML consists of objects—classes—forming a hierarchical structure that allows developers to design and manage UIs in an object-oriented manner. This structure makes it natural for developers to handle XAML directly.
While WPF initially emphasized collaboration between developers and designers, in practice, developers often handle the XAML aspects. This is because XAML goes beyond simple design, forming an object-based hierarchy crucial for implementing sophisticated custom controls. This developer-centric design approach has contributed to XAML becoming a core component in many platforms that emerged after WPF.
Notably, WPF has significantly influenced all XAML-based platforms, serving as a key reference for them.
1.1 Major XAML-Based Frameworks
WPF (Windows Presentation Foundation): A powerful framework for developing Windows desktop applications, offering rich UI and graphic capabilities.
Silverlight: A platform for creating internet applications running in web browsers, now discontinued. It was a lighter version of WPF, operating via plugins. The shift in web standards away from plugin-based platforms led to its decline. To address the shortcomings of Triggers, VisualStateManager (VSM) was introduced in Silverlight 2.
Xamarin.Forms: A mobile app development platform supporting iOS, Android, and Windows. It began as the first XAML-based cross-platform using Mono and was strategically acquired by Microsoft, forming the basis of .NET Core.
UWP (Universal Windows Platform): A platform for developing applications running on Windows 10 and above. It requires Microsoft Store registration and has limitations like restricted WinAPI usage. It supports the same custom control designs as WPF.
WinUI 3: A native UI framework for Windows, serving as the next-generation UI platform for modern Windows app development. It inherits all aspects of UWP while removing constraints and embracing the extensibility of WPF.
MAUI (.NET Multi-platform App UI): A cross-platform UI framework introduced with .NET 6, allowing the development of mobile and desktop apps in a single project.
Uno Platform: A framework that enables the use of UWP and WinUI APIs across various platforms, supporting web (WebAssembly), mobile, and desktop. It supports almost all platforms and provides the same custom control designs as WPF.
Avalonia UI: An open-source UI framework allowing the use of WPF-style XAML in cross-platform environments. It supports the same custom control designs as WPF and extends technology independently to support various platforms.
OpenSilver: An open-source platform optimized for migrating from the old Silverlight to OpenSilver. It operates almost identically to Silverlight and offers a familiar environment for WPF developers.
2. .NET Version Selection Strategy for Cross-Platform Considerations
When developing cross-platform applications, it's crucial to carefully choose the .NET version to use, as it directly impacts compatibility, functionality, and target platform support.
2.1 .NET Version Options
.NET Framework: Windows-only, primarily used in existing WPF and WinForms applications.
.NET Standard 2.0 / 2.1: A standard providing compatibility across various .NET implementations.
.NET (Core) 3.0 and above: A cross-platform .NET implementation supporting Windows, macOS, and Linux, including the latest features and performance improvements.
2.2 Selection Criteria and Considerations
If cross-platform support is needed, .NET Core or the latest .NET should be chosen. If compatibility with existing .NET Framework-based libraries or packages is important, using .NET Standard 2.0 is advisable. For the latest features and performance enhancements, consider .NET 5 or higher.
Moreover, cross-platform frameworks have been considering compatibility from .NET 5.0 onwards, with continuous improvements based on the latest versions. Therefore, choosing the latest .NET version is recommended.
Strategic Suggestions:
Write shared libraries in .NET Standard 2.0 to ensure maximum compatibility.
Create platform-specific projects and reference the shared library.
Use .NET 6 or higher if possible to leverage the latest features and performance improvements.
3. Analysis of View and ViewModel Connection Strategies
In the MVVM (Model-View-ViewModel) pattern, the connection between the View and ViewModel is crucial. The way they are connected significantly influences how MVVM is implemented. Therefore, it's essential to decide on a DataContext assignment method that aligns with how you intend to use MVVM.
In this section, we'll explore various ViewModel connection strategies, analyze their pros and cons, and propose directions for a more sophisticated framework architecture and project design.
3.1 Traditional Direct DataContext Assignment
Creating the ViewModel in the code-behind and directly assigning it to the View's DataContext.
public MainWindow()
{
InitializeComponent();
DataContext = new MainViewModel();
}
Advantages:
Simple and straightforward implementation.
Explicit control over the ViewModel's creation timing.
Ability to pass necessary parameters to the constructor.
Disadvantages:
Creates a strong coupling between the View and ViewModel.
Difficult to mock the ViewModel during unit testing.
Hard to utilize Dependency Injection (DI).
Assigning DataContext directly can lead to inconsistencies in timing.
3.2 Instantiating ViewModel in XAML
Setting the DataContext in XAML to instantiate the ViewModel.
<Window x:Class="MyApp.MainWindow"
xmlns:local="clr-namespace:MyApp.ViewModels">
<Window.DataContext>
<local:MainViewModel />
</Window.DataContext>
<!-- Window content -->
</Window>
Advantages:
IntelliSense support in XAML reduces binding errors.
Allows previewing actual data bindings in the designer.
Explicitly expresses the relationship between the View and ViewModel.
Disadvantages:
Difficult to use Dependency Injection when creating the ViewModel.
Limited in implementing complex initialization logic or parameter passing.
DataContext assignment timing is enforced, reducing flexibility.
3.3 Direct ViewModel Creation with Dependency Passing
Creating the ViewModel in the code-behind and directly passing the required dependencies.
public MainWindow()
{
InitializeComponent();
var dataService = new DataService();
var loggingService = new LoggingService();
DataContext = new MainViewModel(dataService, loggingService);
}
Advantages:
Can explicitly pass the necessary dependencies when creating the ViewModel.
Capable of implementing complex initialization logic.
Flexibility in creating ViewModel instances at runtime.
Disadvantages:
The View must be aware of the ViewModel's dependencies.
The code-behind becomes more complex as dependencies increase.
High coupling between the View and ViewModel persists.
Assigning DataContext directly can lead to inconsistencies in timing.
There's a risk of indiscriminate overuse by freely passing unmanaged dependencies.
3.4 Utilizing a Dependency Injection (DI) Container
Using a DI container to manage the ViewModel and its dependencies can reduce coupling between the View and ViewModel.
public MainWindow()
{
InitializeComponent();
DataContext = ServiceProvider.GetService<MainViewModel>();
}
Advantages:
Reduces coupling between the View and ViewModel.
Centralizes dependency management, enhancing maintainability.
Facilitates unit testing.
Allows for flexible changes in dependencies at runtime.
Disadvantages:
Initial DI container setup can be complex.
Team members need to understand the DI pattern.
Still requires direct creation of the ViewModel in DataContext, potentially leading to inconsistency in assignment timing.
Decisions need to be made on managing ViewModels as Singletons or Instances, considering the View's lifecycle. Clear and precise rules are necessary.
3.5 Automatic ViewModel Creation Strategy in the View
To address the issues above, consider a method where the ViewModel is created via dependency injection at a predetermined point when the View is created, and DataContext is assigned. For example, designing a View that automatically creates the ViewModel based on ContentControl can be effective.
public class UnoView : ContentControl
{
public UnoView()
{
this.Loaded += (s, e) =>
{
var viewModelType = ViewModelLocator.GetViewModelType(this.GetType());
DataContext = ServiceProvider.GetService(viewModelType);
};
}
}
Advantages:
Maintains consistency in DataContext assignment timing.
Reduces coupling between the View and ViewModel.
Automatically handles ViewModel creation and dependency injection.
The View doesn't need to know which ViewModel it should have.
This method virtually has no drawbacks. By managing a single unified View, you can standardize timing and processing logic, ensuring structural improvements and excellent scalability.
However, mapping between Views and ViewModels is necessary. A dictionary or mapping table can be used to centrally manage the connection information between Views and ViewModels. We'll explore methods for managing this mapping in detail in the Bootstrapper Design Methodology section later.
4. Essential Features and Implementation Plans for Framework Design
When designing an application's architecture, it's important to build a framework considering reusability and scalability. Utilizing a Dependency Injection (DI) container is essential for this purpose.
4.1 Utilizing a Dependency Injection (DI) Container
DI is an indispensable pattern in modern software development, greatly aiding in dependency management and reducing coupling. However, in desktop applications like WPF, DI containers commonly used in web applications, such as Microsoft.Extensions.DependencyInjection, may not fit perfectly.
4.1.1 Considerations for Using Microsoft.Extensions.DependencyInjection
Microsoft.Extensions.DependencyInjection is an official DI container provided by .NET and is considered a standard by the .NET Foundation. It's used in almost all systems, including ASP.NET Core, EntityFrameworkCore, and MAUI, offering lifecycle management features like Transient, Scoped, and Singleton.
However, in WPF, the lifecycle management of this standard DI might not align precisely with WPF's actual needs.
Considerations:
Scoped lifecycles may not be necessary in desktop applications like WPF.
Transient and Singleton concepts are designed more for services or web applications, so some features may not fit WPF.
It might introduce unnecessary complexity, and a simpler, lightweight DI container tailored to WPF's use cases could be more appropriate.
While it's possible to use DI without features like Transient, it's important to understand these aspects clearly.
4.1.2 DI in CommunityToolkit.Mvvm
CommunityToolkit.Mvvm does not directly provide DI like Microsoft.Extensions.DependencyInjection. This is because Microsoft.Extensions.DependencyInjection may not align with WPF's lifecycle characteristics.
However, CommunityToolkit.Mvvm allows developers to use their preferred DI container by providing Ioc.Default. It lets you register any DI container implementing the System.IServiceProvider interface.
Therefore, when using CommunityToolkit.Mvvm, you can choose your DI. One of the most commonly used DIs is Microsoft.Extensions.DependencyInjection, and using DI like Prism is also an effective combination.
4.1.3 Benefits of Designing a Custom DI Container
Designing DI directly based on the IServiceProvider interface allows for internal functional integration and compatibility by registering it with Ioc.Default in CommunityToolkit.Mvvm. Since you only need to implement the minimum required methods like GetService, you can create a very simple DI implementation.
Advantages:
Implementing a simple DI container with only the necessary features reduces project complexity.
Allows for internal design, control, and expansion of various features.
Enables you to meticulously build the overall framework architecture and project design.
Provides a consistent DI container not dependent on specific platforms, beneficial for cross-platform development.
Example Code:
// Implementing a DI container based on IServiceProvider
public class SimpleServiceProvider : IServiceProvider
{
private readonly Dictionary<Type, Func<object>> _services = new();
public void AddService<TService>(Func<TService> implementationFactory)
{
_services[typeof(TService)] = () => implementationFactory();
}
public object GetService(Type serviceType)
{
return _services.TryGetValue(serviceType, out var factory) ? factory() : null;
}
}
// Registering and using the DI container
var serviceProvider = new SimpleServiceProvider();
serviceProvider.AddService<IMainViewModel>(() => new MainViewModel());
Ioc.Default.ConfigureServices(serviceProvider);
By implementing a simple DI container based on the IServiceProvider interface, you can register it with Ioc.Default in CommunityToolkit.Mvvm, enabling internal functional integration and compatibility. If using popular DIs like Microsoft.Extensions.DependencyInjection or Prism feels heavy, creating your own can be an attractive choice.
Note:
If you don't adhere to standards like IServiceProvider and System.ComponentModel, you may lose compatibility with Ioc in CommunityToolkit.Mvvm. However, you can utilize CommunityToolkit.Mvvm solely as an MVVM-related module and create a more specialized and consistent DI container not dependent on specific platforms or frameworks. This is quite suitable for creating frameworks that can be commonly used across multiple XAML-based platforms, including cross-platform development.
5. Key Strategies for Effectively Using WPF Technology on Other Platforms
To maximize the powerful features of WPF on other XAML-based platforms, it's essential to understand some history and core strategies. Additionally, it's necessary to grasp the characteristics of platforms where WPF technology can be used as is.
5.1 Understanding Platform Characteristics and Differences
Differences between UWP and WinUI 3: UWP is a platform for Windows 10, requiring adherence to store registration guidelines and imposing WinAPI restrictions, making it challenging to be compatible with WPF or WinForms legacy. WinUI 3 emerged to inherit all the strengths of UWP while addressing its issues, evolving into a platform with the high degree of freedom like WPF.
Similarity between Uno Platform Desktop and WinUI 3: The desktop platforms of Uno Platform covering Windows, macOS, and Linux follow the same approach as WinUI 3. Therefore, since WinUI 3 uses UWP's core libraries identically, and Uno Platform also follows WinUI 3's approach, they share all DLL libraries starting with
Microsoft.*
.
Understanding these platform characteristics reveals how effective and attractive the Uno Platform Desktop is. Therefore, sharing and transitioning technology between WPF and Uno Platform, which also connects to WinUI 3 and UWP, is a highly effective and efficient strategy.
5.2 Active Utilization of VisualStateManager (VSM)
Since you cannot directly use WPF's Trigger on all platforms, a strategy to replace it is necessary. VisualStateManager (VSM) plays a key role in solving this problem.
VSM was introduced in Silverlight 2.0 to compensate for the shortcomings of Triggers, optimized for state processing between custom controls and XAML. Later, VSM was also introduced in .NET 4.0 for WPF, and the internal design of all custom controls in WPF, including Button, CheckBox, DataGrid, and Window, was changed from Triggers to VSM.
Advantages:
Enables you to implement the same functionality as Triggers on platforms where they can't be used directly.
Allows effective implementation of UI state management and animations.
Can unify different behaviors across platforms through VSM.
By focusing on using VSM, you can construct the same XAML and custom controls across platforms like WPF, Uno Platform Desktop, WinUI 3, and UWP, and share the source code identically.
5.3 Flexible Use of IValueConverter
IValueConverter is an interface that allows value conversion during data binding, useful for abstracting differences between platforms.
Strategic Use:
Can implement and replace functionalities almost identical to Triggers, allowing you to write effective and straightforward source code.
Since you need to create a Converter each time and the criteria for reusability are ambiguous, it's better to use them flexibly without being too tied to reusability.
Even without reusability, it's important to use them intuitively, naming them clearly to minimize branching and use them in a specialized manner.
Limitations and Complementation:
Using only IValueConverter for everything has limitations.
IValueConverter is suitable for simple conversions; managing complex scenarios can be burdensome, so VSM should be used for such cases.
For complex state management, it's better to use the VisualStateManager.
In conclusion, IValueConverter complements the shortcomings of VSM, and it's important to use them flexibly without overemphasizing reusability for simple and straightforward conversion tasks.
6. Bootstrapper Design Methodology for Distributed Project Management
As applications become more complex and modularized, initialization processes and dependency management become crucial. The Bootstrapper pattern is useful for centralizing and managing these initialization logics.
While all platforms base their design on Application, each platform has different characteristics and methods, resulting in varied Application designs. Therefore, using a Bootstrapper structure is effective for maintaining the same development approach across all platforms.
6.1 Role and Necessity of Bootstrapper
Functions of Bootstrapper:
Dependency Injection Setup: Initializes the DI container and registers necessary services, views, and ViewModels.
Managing View and ViewModel Connections: Registers views via dependency injection and manages mappings between views and ViewModels.
Centralized Configuration Management: Manages all configurations in the Bootstrapper, allowing the application project to focus solely on its role while other functionalities are managed through project distribution and modularization.
Additionally, you can flexibly expand centralized management items without affecting the Application.
Advantages:
Separates initialization logic of the application, enhancing code readability and maintainability.
Allows independent development of functionalities through project distribution and modularization.
Minimizes structural differences between platforms, maintaining a consistent architecture.
6.2 Design Plan of Bootstrapper
Example Code:
namespace Jamesnet.Core;
public abstract class AppBootstrapper
{
protected readonly IContainer Container;
protected readonly ILayerManager Layer;
protected readonly IViewModelMapper ViewModelMapper;
protected AppBootstrapper()
{
Container = new Container();
Layer = new LayerManager();
ViewModelMapper = new ViewModelMapper();
ContainerProvider.SetContainer(Container);
ConfigureContainer();
}
protected virtual void ConfigureContainer()
{
Container.RegisterInstance<IContainer>(Container);
Container.RegisterInstance<ILayerManager>(Layer);
Container.RegisterInstance<IViewModelMapper>(ViewModelMapper);
Container.RegisterSingleton<IViewModelInitializer, DefaultViewModelInitializer>();
}
protected abstract void RegisterViewModels();
protected abstract void RegisterDependencies();
public void Run()
{
RegisterViewModels();
RegisterDependencies();
OnStartup();
}
protected abstract void OnStartup();
}
By emphasizing management structure through abstraction and controlling timing and order via virtual methods, you can facilitate flexible expansion and complementation. This allows the Application to remain unaffected while operating consistently across various platforms.
7. Strategies to Maximize WPF Technology in Desktop Cross-Platform Environments
By leveraging WPF technologies and patterns in other XAML-based cross-platform frameworks, you can significantly enhance development efficiency.
7.1 Implementing a Framework That Operates on All Platforms
Jamesnet.Core is a framework based on .NET Standard 2.0, enabling the same project design across WPF, Uno Platform, and WinUI 3. This framework has the following features:
DI Design: Utilizes a DI container based on IServiceProvider, allowing integration with CommunityToolkit.Mvvm.
MVVM Bootstrapper: Centralizes project initialization and dependency injection.
Managing View and ViewModel Connections: Reduces coupling between views and ViewModels through layer management for view injection.
Designed to Operate Consistently across all XAML-based platforms.
Directly referenced in the repository source code, facilitating debugging, feature implementation, extension, and research.
Advantages:
Maintains the same architecture regardless of developing with WPF, Uno Platform, or WinUI 3.
Using Uno Platform Desktop allows development and execution on macOS and Linux.
JetBrains Rider can be used to build a cross-platform development environment.
7.2 Analysis of Actual Implementation Cases
The League of Legends Client Reconstruction Project was implemented using the Jamesnet.Core framework across WPF, Uno Platform, and WinUI 3, using the same codebase and architecture.
WPF Version: GitHub - leagueoflegends-wpf
Uno Platform Version: GitHub - leagueoflegends-uno
WinUI 3 Version: GitHub - leagueoflegends-winui3
Strategic Approach:
Maintained consistency in project design through the Jamesnet.Core framework.
Centralized management of views and ViewModels using the DI container and Bootstrapper.
Replaced Triggers with VisualStateManager (VSM), managing UI states in the same way across different platforms.
Outcomes:
Shared over 97% of the code, maximizing scalability to other platforms.
Presented consistent user experiences and development methodologies across various platforms, facilitating easy technological transition to cross-platform environments.
Significantly reduced development and maintenance costs through project distribution, modularization, and centralized management.
Improved refactoring and scalability by developing modular CustomControls, and enhanced effectiveness in utilizing AI like GPT and Claude due to the distributed view structure.
Conclusion
WPF technologies and patterns remain powerful, and applying them to cross-platform development can enhance development efficiency and code reusability. Utilizing the Jamesnet.Core framework, especially the centralized management strategy of the DI container and the introduction of the Bootstrapper, greatly helps in reducing coupling between views and ViewModels and improving maintainability.
Moreover, actively leveraging the VisualStateManager and IValueConverter can minimize platform differences and maintain consistent designs. Through these strategies, you can achieve technological expansion beyond WPF bases to cross-platform environments strategically.
Notably, since UWP, WinUI 3, and Uno Platform use XAML-related DLLs identically, there are virtually no differences between these platforms. Therefore, for WPF developers, using the Uno Platform Desktop is highly effective and strategic. Transitioning from WPF to Uno can be done within hours, and converting to WinUI 3 is also very feasible.
Moving forward, WPF technology and XAML-based frameworks will continue to evolve, and cross-platform development leveraging them will become increasingly important. Developers should understand these trends well and establish appropriate strategies to develop high-quality applications.
References
Major Repositories
Jamesnet.Core Framework: GitHub - jamesnet.core
- A framework operating on all XAML-based platforms, providing DI, MVVM, Bootstrapper, etc.
League of Legends Client Reconstruction Project:
WPF Version: GitHub - leagueoflegends-wpf
Uno Platform Version: GitHub - leagueoflegends-uno
WinUI 3 Version: GitHub - leagueoflegends-winui3
DynamicResource Theme: GitHub - dynamic-theme
- A library for dynamic resource theme changes in WPF.
Main Channels
YouTube: Jamesnet
- Provides various video materials on WPF, XAML, and cross-platform development.
Bilibili: Jamesnet Bilibili Channel
- Chinese version of YouTube content, offering a range of development-related videos.
Website: Jamesnet.dev
- Shares technical blogs, seminar materials, projects, etc.
WPF Tutorials (Custom Control)
Seminars and Lectures
-
- Access to various seminar and lecture materials.
Thank you.
Subscribe to my newsletter
Read articles from Jaewung Lee directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by