基於XAML框架和跨平台專案架構設計的深入技術分析

基於XAML框架和跨平台專案架構設計的深入技術分析

我們深入探討了基於XAML的各種平台、跨平台策略以及為有效的專案架構設計所需的核心技術。

最後更新 2024/11/8 下午10:03
Vicky&James
預計閱讀 20 分鐘
分類
.NET
標籤
.NET C# WPF 架構設計 跨平台

本文基於Vicky&James 2024年10月22日在韓國Microsoft總部BMW meetup會議上的演講內容重新整理而成。這次研討會我們深入探討了基於XAML的各種平台、跨平台策略以及為有效的專案架構設計所需的核心技術。

介紹

大家好,我們中韓Microsoft MVP夫妻Vicky&James。我們從WPF開始就對包括Uno Platform在內的基於XAML的框架和專案架構設計有著深入的興趣與經驗。大家可以在我們的GitHub倉庫中查看和下載各種專案的原始碼:GitHub - jamesnetgroup

目錄

  1. XAML平台與跨平台概述
  2. 考量跨平台的.NET版本選擇策略
  3. View與ViewModel的連接策略分析
  4. 框架設計的必要功能及實作方案
  5. 在其他平台上有效使用WPF技術的核心策略
  6. 分散式專案管理的Bootstrapper設計方法論
  7. 在桌面跨平台中最大化利用WPF技術的策略

1. XAML平台與跨平台概述

XAML是一種用於以宣告方式定義UI的標記語言,被用於多個平台。XAML具有由物件(即類別)組成的階層結構,使開發人員能夠以物件導向的方式設計與管理UI。由於這種結構,開發人員直接處理XAML是很自然的。

當WPF首次出現時,它強調了開發人員與設計師之間的協作,但實際上XAML領域通常也由開發人員負責。這是因為XAML不僅僅是簡單的設計,而是形成了基於物件的階層結構,在複雜的自訂控制項實作中也發揮著重要作用。這種面向開發人員的設計方法促使XAML不僅在WPF中,而且在隨後出現的許多平台中都成為核心元件。

特別是,WPF對所有基於XAML的平台都產生了重大影響,並成為這些平台最重要的參考。

1.1 主要的XAML框架

  • WPF:用於Windows桌面應用程式開發的強大框架,提供豐富的UI與圖形功能。
  • Silverlight:用於在web瀏覽器中執行的網際網路應用程式的平台,目前已停止支援。它是WPF的輕量級版本,以插件方式執行。由於Web標準政策的變化,基於插件的Web平台消失,在Silverlight 2中引入了VisualStateManager(VSM) 來彌補Trigger的缺點。
  • Xamarin.Forms:支援iOS、Android與Windows的行動應用開發平台。作為基於Mono的第一個基於XAML的跨平台,被Microsoft策略性收購並成為.NET Core的基礎。
  • UWP(Universal Windows Platform):用於開發執行在Windows 10及以上版本的應用程式的平台。需要註冊Microsoft Store,並有WinAPI使用限制等約束。支援與WPF相同的自訂控制項設計。
  • WinUI 3:Windows原生UI框架,是最新Windows應用開發的下一代UI平台。繼承了UWP的所有優點,同時解決了其限制,並採納了WPF的可擴展性。
  • MAUI(.NET Multi-platform App UI):從.NET 6開始引入的跨平台UI框架,可以在單一專案中開發行動與桌面應用。
  • Uno Platform:允許在各種平台上使用UWP與WinUI的API的框架,支援Web(WebAssembly)、行動與桌面。支援幾乎所有平台,並提供與WPF相同的自訂控制項設計。
  • Avalonia UI:允許在跨平台上使用WPF風格XAML的開源UI框架。支援與WPF相同的自訂控制項設計,透過獨特的技術擴展支援各種平台。
  • OpenSilver:為將舊的Silverlight遷移到OpenSilver而優化的開源平台。以與Silverlight幾乎相同的方式執行,也為WPF開發人員提供熟悉的環境。

2. 考量跨平台的.NET版本選擇策略

在跨平台應用程式開發中,需要謹慎選擇要使用的.NET版本。因為這將直接影響相容性、功能性與目標平台支援。

2.1 .NET版本選項

  • .NET Framework:僅用於Windows,主要用於現有的WPF與WinForms應用程式。
  • .NET Standard 2.0 / 2.1:為提供各種.NET實作之間的相容性而設計的標準。
  • .NET (Core) 3.0及以上:支援Windows、macOS、Linux的跨平台.NET實作,包含最新功能與效能改進。

2.2 選擇標準與考量因素

如果需要跨平台支援,應該選擇.NET Core或最新的.NET。如果與現有.NET Framework函式庫或套件的相容性很重要,那麼則應該使用.NET Standard 2.0。如果想要最新功能與效能改進,就需要考慮.NET 5及以上版本。

此外,跨平台框架從.NET 5.0開始考慮相容性,並且基於最新版本持續進行功能改進,因此建議大家選擇最新的.NET版本。

策略建議

  • 將通用函式庫編寫為.NET Standard 2.0以確保最大相容性。
  • 為每個平台建立專案並引用通用函式庫。
  • 如果可能,使用.NET 6及以上版本以獲得最新功能與效能改進。

3. View與ViewModel的連接策略分析

在MVVM模式中,View與ViewModel的連接是核心部分。連接方式的不同會導致使用MVVM的方式完全不同。因此,我們需要根據使用MVVM的目的來決定DataContext分配方式。

3.1 傳統的DataContext直接分配方式

在程式碼後置建立ViewModel並直接分配給View的DataContext。

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

優點

  • 實作簡單直觀
  • 可以明確控制檢視模型的建立時機
  • 可以向建構函式傳遞所需參數

缺點

  • View與ViewModel之間產生強耦合
  • 單元測試時難以模擬(Mock)ViewModel
  • 難以利用相依性注入(DI)
  • 需要直接指定DataContext分配時機,可能難以保持一致性

3.2 在XAML中建立ViewModel實例

在XAML中設定DataContext來實例化ViewModel。

<Window x:Class="MyApp.MainWindow"
        xmlns:local="clr-namespace:MyApp.ViewModels">
    <Window.DataContext>
        <local:MainViewModel />
    </Window.DataContext>
    <!-- Window content -->
</Window>

優點

  • XAML的智慧感知支援可以減少繫結錯誤
  • 可以在設計器中預覽實際的資料繫結
  • 明確表達View與ViewModel之間的關係

缺點

  • ViewModel建立時難以進行相依性注入
  • 複雜的初始化邏輯或參數傳遞受限
  • DataContext分配時機被強制化,降低了靈活性

3.3 ViewModel直接建立及相依傳遞

在程式碼後置建立ViewModel時直接傳遞所需的相依。

public MainWindow()
{
    InitializeComponent();
    var dataService = new DataService();
    var loggingService = new LoggingService();
    DataContext = new MainViewModel(dataService, loggingService);
}

優點

  • 可以在建立ViewModel時明確傳遞所需的相依
  • 可以實作複雜的初始化邏輯
  • 可以根據Runtime靈活建立ViewModel實例

缺點

  • View需要了解ViewModel的相依關係
  • 隨著相依增加,程式碼後置會變得複雜
  • View與ViewModel之間的耦合度仍然很高
  • 需要直接指定DataContext分配時機,可能難以保持一致性
  • 由於不使用DI管理專案,可能會出現相依關係混亂的情況

3.4 善用相依性注入(DI)容器

使用DI容器來管理ViewModel及其相依可以降低View與ViewModel之間的耦合度。

public MainWindow()
{
    InitializeComponent();
    DataContext = ServiceProvider.GetService<MainViewModel>();
}

優點

  • 降低View與ViewModel之間的耦合度
  • 相依關係集中管理,提高可維護性
  • 便於進行單元測試
  • 可以在執行時靈活更改相依關係

缺點

  • DI容器的初始配置可能較為複雜
  • 團隊成員需要理解DI模式
  • 仍然需要直接在DataContext中建立檢視模型,分配時機的一致性可能難以保持
  • 需要決定是將檢視模型作為單例還是實例來管理,並考慮檢視的生命週期。需要制定明確的規則並嚴格遵守

3.5 View的自動ViewModel建立策略

為了解決上述問題,我們可以考慮在View建立的約定時間點,透過相依性注入建立ViewModel並且分配給DataContext。例如,設計一個基於ContentControl的View,自動建立ViewModel就是一個有效的方法。

public class UnoView : ContentControl
{
    public UnoView()
    {
        this.Loaded += (s, e) =>
        {
            var viewModelType = ViewModelLocator.GetViewModelType(this.GetType());
            DataContext = ServiceProvider.GetService(viewModelType);
        };
    }
}

優點

  • 可以保持DataContext分配時機的一致性
  • 降低View與ViewModel之間的耦合度
  • ViewModel的建立與相依性注入自動處理
  • View不需要知道自己應該使用哪個ViewModel

這幾乎是一個沒有缺點的方法,透過管理單一的View,可以統一處理時機與處理邏輯。在結構性完善與擴展方面也能保證很好的設計。

不過需要View與ViewModel之間的Mapping,可以使用Dictionary或Mapping Table來實現。這樣可以集中管理View與ViewModel之間的連接資訊。關於連接管理的映射方法,我們將在後面的Bootstrapper設計方法論中詳細討論。

4. 框架設計的必要功能及實作方案

在設計應用程式架構時,建構考慮可重用性與可擴展性的框架非常重要。為此,使用相依性注入(DI)容器是必不可少的。

4.1 相依性注入(DI)容器的使用

DI是現代軟體開發中不可或缺的模式,對相依關係管理與降低耦合度有很大幫助。然而,在像WPF這樣的桌面應用程式中,Web應用程式中常用的Microsoft.Extensions.DependencyInjection等DI容器可能並不完全適合。

4.1.1 Microsoft.Extensions.DependencyInjection的使用與注意事項

Microsoft.Extensions.DependencyInjection是.NET官方提供的DI容器,可以說是.NET Foundation的標準。它被用於ASP.NET Core、EntityFrameworkCore、MAUI等各種平台與框架中的幾乎所有系統中使用,並提供TransientScopedSingleton等生命週期管理功能。

然而,在WPF中,這種標準DI的生命週期可能與WPF實際情況並不完全匹配。

注意事項

  • 在WPF等桌面應用程式中,可能不需要Scoped生命週期
  • TransientSingleton的概念是為服務或Web應用程式設計的,在WPF中某些功能可能不適用
  • 可能帶來不必要的複雜性,對於WPF的使用場景來說,更簡單輕量的DI容器可能更合適

當然,即使不使用Transient這樣的生命週期也可以使用DI,但準確理解這些要點是非常重要的。

4.1.2 CommunityToolkit.Mvvm的DI

CommunityToolkit.Mvvm並不直接提供像Microsoft.Extensions.DependencyInjection這樣的DI。這可能是因為Microsoft.Extensions.DependencyInjection與WPF的生命週期特性不完全匹配。

但是,CommunityToolkit.Mvvm透過提供Ioc.Default允許開發者使用任何想要的DI容器。任何實作了System.IServiceProvider介面的DI容器都可以註冊使用。

因此,使用CommunityToolkit.Mvvm時可以選擇DI。最常用的DI之一無疑是Microsoft.Extensions.DependencyInjection,使用Prism這樣的DI也是非常有效的組合。

4.1.3 直接設計DI容器的優勢

基於IServiceProvider介面設計DI的方法可以註冊到CommunityToolkit.MvvmIoc.Default中,實現內部功能的連接與相容。由於IServiceProvider只要求實作GetService等最基本的功能,因此可以實現非常簡單的DI。

優點

  • 實作只包含必要功能的簡單DI容器,降低專案複雜性
  • 可以在內部設計、控制與擴展各種功能
  • 可以精確建構整體框架架構與專案設計
  • 提供不依賴特定平台的統一DI容器,有利於跨平台開發

範例程式碼

// 基於IServiceProvider的DI容器實作
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;
    }
}

// DI容器註冊與使用
var serviceProvider = new SimpleServiceProvider();
serviceProvider.AddService<IMainViewModel>(() => new MainViewModel());

Ioc.Default.ConfigureServices(serviceProvider);

這樣基於IServiceProvider介面實現簡單的DI容器,就可以註冊到CommunityToolkit.MvvmIoc.Default中,實現內部功能的連接與相容。如果覺得使用Microsoft.Extensions.DependencyInjectionPrism等主流DI太繁重,自己直接來實現一個是非常有吸引力的選擇。

注意: 如果不遵循IServiceProviderSystem.ComponentModel標準,可能會失去與CommunityToolkit.MvvmIoc相容性。但是我們可以將CommunityToolkit.Mvvm僅作為MVVM相關的模組,建立一個更專業、更統一的、不依賴特定平台或框架的DI容器。這對於建立可以在跨平台等多個XAML平台上通用的框架是非常適合的。

5. WPF技術在其他平台上的有效使用策略

要在其他XAML平台上最大限度地利用WPF強大的功能,我們需要了解一些歷史背景與核心策略。同時也需要詳細了解可以直接使用WPF技術的平台特性。

5.1 平台間的特徵與差異理解

  • UWP與WinUI 3的差異:UWP作為Windows 10的專用平台,由於應用商店註冊指南與WinAPI限制等原因,與WPF和WinForms等傳統平台的相容性較差。因此,WinUI 3應運而生,它不僅繼承了UWP的所有優點,還改進了其存在的問題,發展成為了一個像WPF一樣具有高自由度的平台。

  • Uno Platform桌面版與WinUI 3的一致性:Uno Platform支援Windows、macOS、Linux的桌面平台完全遵循WinUI 3的方式。因此,WinUI 3直接使用UWP的核心庫,而Uno Platform也同樣採用WinUI 3的方式,這意味著所有以Microsoft.*開頭的DLL庫都可以共享使用。

理解這些平台間的特徵可以讓我們認識到Uno Platform Desktop是一個非常高效且具有吸引力的平台。因此,在WPF與Uno Platform之間進行技術共享與轉換的策略非常有效且高效,因為它們與WinUI 3和UWP都有著緊密的聯繫。

5.2 充分利用VisualStateManager(VSM)

由於不是所有平台都可以直接使用WPF的Trigger,所以我們就需要一個替代策略。VisualStateManager(VSM)在解決這個問題上起著核心作用。

VSM是在Silverlight 2.0中引入的,用來彌補Trigger不足的,對自訂控制項與XAML之間的狀態處理進行了最佳化。隨後在.NET 4.0中,VSM也被引入到WPF中,WPF的Button、CheckBox、DataGrid、Window等所有CustomControl的內部設計都從Trigger改為了VSM。

優點

  • 在不能直接使用Trigger的平台上可以透過VSM實現相同功能
  • 可以有效實現UI狀態管理與動畫
  • 可以透過VSM統一不同平台的不同行為

最終,透過集中使用VSM,就可以實現在WPF、Uno Platform Desktop、WinUI 3、UWP等平台上建構相同的XAML與CustomControl,原始碼也可以完全共享。

5.3 靈活運用IValueConverter

IValueConverter是一個允許在資料繫結時轉換值的介面,對於抽象化平台間差異非常有用。

策略性使用

  • 可以實現和替代幾乎與Trigger相同的功能,編寫簡單有效的原始碼
  • 由於每次都需要建立Converter,而且重用性標準模糊,建議不要過分追求重用性,而是靈活使用
  • 即使沒有重用性也要直觀使用,重要的是透過明確的命名來盡量減少分支,專門化使用

局限與補充

  • 僅使用IValueConverter是有限的
  • IValueConverter應用於簡單轉換,複雜場景的管理會帶來負擔,這時我們應透過VSM解決
  • 複雜的狀態處理最好使用VisualStateManager

總之,IValueConverter補充了VSM的不足,對於簡單直接的轉換工作應該直觀靈活使用,不要過分追求重用性。

6. 分散式專案管理的Bootstrapper設計方法論

隨著應用程式變得複雜與模組化,初始化過程與相依管理變得越來越重要。Bootstrapper模式在集中管理這些初始化邏輯方面非常有用。

雖然所有平台都以Application設計為基礎,但由於各平台的特特性與方式不同,Application設計各不相同。因此,為了在所有平台上保持相同的開發方式,使用Bootstrapper結構設計是非常有效的。

6.1 Bootstrapper的作用與必要性

Bootstrapper的功能

  • 相依性注入設定:初始化DI容器,註冊必要的服務、View與ViewModel。
  • 管理檢視與檢視模型的連接:透過相依性注入註冊View,管理View與ViewModel之間的映射。
  • 集中化配置管理:所有配置都在Bootstrapper中管理,使應用程式專案只執行其角色,其餘功能實現透過專案分散式與模組化來管理。
  • 此外,還可以靈活地擴展集中化管理項目,且不會影響Application。

優點

  • 透過分離應用程式的初始化邏輯來提高程式碼的可讀性與可維護性。
  • 透過專案分散式與模組化,可以獨立開發功能實現。
  • 最小化平台之間的結構差異,保持一致的架構。

6.2 Bootstrapper的設計方案

範例程式碼

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();
}

透過這樣的抽象化,可以明確強調管理結構,並透過虛方法控制時間點與順序。這有助於靈活擴展與完善,並且不影響Application,使其在各種平台上以一致的方式執行。

7. 在跨平台桌面環境中最大化WPF技術的策略

透過在其他基於XAML的跨平台框架中最大限度地利用WPF中使用的技術與模式,可以提高開發效率。

7.1 實作可在所有平台上執行的框架

Jamesnet.Core是基於.NET Standard 2.0的框架,允許在WPF、Uno Platform與WinUI 3中實現相同的專案設計。該框架具有以下特點:

  • DI設計:利用基於IServiceProvider的DI容器,可以與CommunityToolkit.Mvvm配合使用。
  • MVVM Bootstrapper:集中管理專案的初始化與相依性注入。
  • View與ViewModel之間的連接管理:透過層管理等方式降低View與ViewModel的耦合度。
  • 在所有基於XAML的平台上統一執行。
  • 直接引用儲存庫原始碼,便於偵錯、功能實現、擴展與研究。

優點

  • 無論使用WPF、Uno Platform還是WinUI 3開發,都可以保持相同的架構。
  • 使用Uno Platform Desktop可以在macOS與Linux上進行開發與執行。
  • 可以使用JetBrains Rider建構跨平台開發環境。

7.2 實際實作案例分析

英雄聯盟客戶端重構專案利用Jamesnet.Core框架,在WPF、Uno Platform與WinUI 3等不同平台上使用相同的程式碼庫與架構實現。

策略方法

  • 透過Jamesnet.Core框架保持專案設計的統一性
  • 利用DI容器與Bootstrapper集中管理檢視與檢視模型。
  • 使用VisualStateManager(VSM)替代Trigger,在不同平台上以相同方式管理UI狀態。

成果

  • 97%以上的程式碼共享,最大化了向其他平台擴展的可能性。
  • 在各種平台上提供一致的使用者體驗與開發方法論,使技術轉換更容易。
  • 透過專案分散化、模組化與管理集中化,大大降低了開發與維護成本。
  • 透過CustomControl的模組化開發提高了重構與擴展性,在GPT、Claude等AI應用方面,分散的檢視系統也更加有效。

結論

WPF技術與模式仍然強大,將其應用於跨平台開發可以提高開發效率與程式碼重用性。特別是使用Jamesnet.Core框架,透過DI容器的集中化管理策略與引入Bootstrapper,有助於降低檢視與檢視模型之間的耦合度,提高可維護性。

此外,透過積極使用VisualStateManagerIValueConverter,可以最小化平台之間的差異並保持一致的設計。透過這些策略,可以超越WPF基礎,策略性地實現跨平台技術擴展。

特別是,UWPWinUI 3Uno Platform之間100%相同地使用XAML相關DLL,因此這些平台之間幾乎沒有差異。因此,對WPF開發者來說,使用Uno Platform桌面版非常有效且具有策略意義。這是因為從WPF轉換到Uno可以在幾小時內完成,轉換到WinUI 3也非常容易。

未來,WPF技術與基於XAML的框架將繼續發展,利用這些進行跨平台開發將變得更加重要。開發人員需要很好地把握這些趨勢,制定適當的策略,開發高品質的應用程式。

參考

主要倉庫

目前已更新的WPF教學(自訂控制項)

繼續探索

延伸閱讀

更多文章
同分類 / 同標籤 2025/5/27

WPF完成一個危險提醒效果

當我們寫的程式發放出去後,用戶是在進行一些危險操作,我們的軟體應該給一些提醒效果,比如邊框邊緣有紅色,類似與高德地圖那樣的警報提醒效果

繼續閱讀
同分類 / 同標籤 2024/6/20

CodeWF.EventBus:輕量級事件匯流排,讓通訊更流暢

CodeWF.EventBus,一款靈活的事件匯流排庫,實現模組間解耦通訊。支援多種.NET專案類型,如WPF、WinForms、ASP.NET Core等。採用簡潔設計,輕鬆實現命令的發布與訂閱、請求與回應。透過有序的事件處理,確保事件得到妥善處理。簡化您的程式碼,提升系統可維護性。

繼續閱讀