控件名称:leagueoflegends-uno/wpf/winui3 作者:Vicky&James leagueoflegends-wpf[1]:https://github.com/jamesnetgroup/leagueoflegends-wpf leagueoflegends-uno[2]:https://github.com/jamesnetgroup/leagueoflegends-uno leagueoflegends-winui3[3]:https://github.com/jamesnetgroup/leagueoflegends-winui3
本文基于Vicky&James 2024年10月22日在韩国Microsoft总部BMW meetup会议上的演讲内容重新整理而成。这次研讨会我们深入探讨了基于XAML的各种平台、跨平台战略以及为有效的项目架构设计所需的核心技术。
大家好,我们中韩Microsoft MVP夫妇Vicky&James。我们从WPF开始就对包括Uno Platform在内的基于XAML的框架和项目架构设计有着深入的兴趣和经验。大家可以在我们的GitHub仓库中查看和下载各种项目的源代码:GitHub - jamesnetgroup[4]
XAML是一种用于以声明方式定义UI的标记语言,被用于多个平台。XAML具有由对象(即类)组成的层次结构,使开发人员能够以面向对象的方式设计和管理UI。由于这种结构,开发人员直接处理XAML是很自然的。
当WPF首次出现时,它强调了开发人员和设计师之间的协作,但实际上XAML领域通常也由开发人员负责。这是因为XAML不仅仅是简单的设计,而是形成了基于对象的层次结构,在复杂的自定义控件实现中也发挥着重要作用。这种面向开发人员的设计方法促使XAML不仅在WPF中,而且在随后出现的许多平台中都成为核心组件。
特别是,WPF对所有基于XAML的平台都产生了重大影响,并成为这些平台最重要的参考。
在跨平台应用程序开发中,需要谨慎选择要使用的.NET版本。因为这将直接影响兼容性、功能性和目标平台支持。
如果需要跨平台支持,应该选择.NET Core
或最新的.NET
。如果与现有.NET Framework库或包的兼容性很重要,那么则应该使用.NET Standard 2.0
。如果想要最新功能和性能改进,就需要考虑.NET 5及以上
版本。
此外,跨平台框架从.NET 5.0开始考虑兼容性,并且基于最新版本持续进行功能改进,因此建议大家选择最新的.NET版本。
战略建议:
.NET Standard 2.0
以确保最大兼容性。.NET 6及以上
版本以获得最新功能和性能改进。在MVVM(Model-View-ViewModel)模式中,View和ViewModel的连接是核心部分。连接方式的不同会导致使用MVVM的方式完全不同。因此,我们需要根据使用MVVM的目的来决定DataContext分配方式。
在代码后台创建ViewModel并直接分配给View的DataContext。
public MainWindow()
{
InitializeComponent();
DataContext = new MainViewModel();
}
优点:
缺点:
在XAML中设置DataContext来实例化ViewModel。
<Window x:Class="MyApp.MainWindow"
xmlns:local="clr-namespace:MyApp.ViewModels">
<Window.DataContext>
<local:MainViewModel />
</Window.DataContext>
<!-- Window content -->
</Window>
优点:
缺点:
在代码后台创建ViewModel直接创建及依赖传递时直接传递所需的依赖。
public MainWindow()
{
InitializeComponent();
var dataService = new DataService();
var loggingService = new LoggingService();
DataContext = new MainViewModel(dataService, loggingService);
}
优点:
缺点:
使用DI容器来管理ViewModel及其依赖可以降低View和ViewModel之间的耦合度。
public MainWindow()
{
InitializeComponent();
DataContext = ServiceProvider.GetService<MainViewModel>();
}
优点:
缺点:
为了解决上述问题,我们可以考虑在View创建创建的约定时间点,通过依赖注入创建ViewModel并且分配给DataContext。例如,设计一个基于ContentControl的Veiw,自动创建ViewModel就是一个有效的方法。
public class UnoView : ContentControl
{
public UnoView()
{
this.Loaded += (s, e) =>
{
var viewModelType = ViewModelLocator.GetViewModelType(this.GetType());
DataContext = ServiceProvider.GetService(viewModelType);
};
}
}
优点:
这几乎是一个没有缺点的方法,通过管理单一的View,可以统一处理时机和处理逻辑。在结构性完善和扩展方面也能保证很好的设计。
不过需要View和ViewModel之间的Mapping,可以使用Dictionary或Mapping Table 来实现。这样可以集中管理View和ViewModel之间的连接信息。关于连接管理的映射方法,我们将在后面的Bootstrapper设计方法论
中详细讨论。
在设计应用程序架构时,构建考虑可重用性和可扩展性
的框架非常重要。为此,使用依赖注入(DI)容器是必不可少的。
DI是现代软件开发中不可或缺的模式,对依赖关系管理和降低耦合度有很大帮助。然而,在像WPF这样的桌面应用程序中,Web应用程序中常用的Microsoft.Extensions.DependencyInjection
等DI容器可能并不完全适合。
Microsoft.Extensions.DependencyInjection
是.NET官方提供的DI容器,可以说是.NET Foundation的标准。它被用于ASP.NET Core、EntityFrameworkCore、MAUI等各种平台和框架中的几乎所有系统中使用,并提供Transient
、Scoped
、 Singleton
等生命周期管理功能。
然而,在WPF中,这种标准DI的生命周期可能和WPF实际情况并不完全匹配。
注意事项:
Scoped
生命周期Transient
或Singleton
的概念是为服务或Web应用程序设计的,在WPF中某些功能可能不适用当然,即使不使用Transient
这样的生命周期也可以使用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也是非常有效的组合。
基于IServiceProvider
接口设计DI的方法可以注册到CommunityToolkit.Mvvm
的Ioc.Default
中,实现内部功能的连接和兼容。由于IServiceProvider
只要求实现GetService
等最基本的功能,因此可以实现非常简单的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.Mvvm
的Ioc.Default
中,实现内部功能的连接和兼容。如果觉得使用Microsoft.Extensions.DependencyInjection
、Prism
等主流DI太繁重,自己直接来实现一个是非常有吸引力的选择。
注意: 如果不遵循IServiceProvider
等System.ComponentModel
标准,可能会失去和CommunityToolkit.Mvvm
的Ioc
兼容性。但是我们可以将CommunityToolkit.Mvvm
仅作为MVVM相关的模块,创建一个更专业、更统一的、不依赖特定平台或框架的DI容器。这对于创建可以在跨平台等多个XAML平台上通用的框架是非常适合的。
要在其他XAML平台上最大限度地利用WPF强大的功能,我们需要了解一些历史背景和核心策略。同时也需要详细了解可以直接使用WPF技术的平台特性。
Microsoft.*
开头的DLL库都可以共享使用。理解这些平台间的特征可以让我们认识到Uno Platform Desktop
是一个非常高效且具有吸引力的平台。因此,在WPF和Uno Platform之间进行技术共享和转换的策略非常有效且高效,因为它们与WinUI 3和UWP都有着紧密的联系。
由于不是所有平台都可以直接使用WPF的Trigger,所以我们就需要一个替代策略。VisualStateManager(VSM)
在解决这个问题上起着核心作用。
VSM是在Silverlight 2.0中引入的,用来弥补Trigger不足的,对自定义控件和XAML之间的状态处理进行了优化。随后在.NET 4.0中,VSM也被引入到WPF中,WPF的Button、CheckBox、DataGrid、Window等所有CustomControl的内部设计都从Trigger改为了VSM。
优点:
最终,通过集中使用VSM,就可以实现在WPF、Uno Platform Desktop、WinUI 3、UWP等平台上构建相同的XAML和CustomControl,源代码也可以完全共享。
IValueConverter
是一个允许在数据绑定时转换值的接口,对于抽象化平台间差异非常有用。
策略性使用:
局限和补充:
IValueConverter
是有限的IValueConverter
应用于简单转换,复杂场景的管理会带来负担,这时我们应通过VSM
解决VisualStateManager
总之,IValueConverter
补充了VSM的不足,对于简单直接的转换工作应该直观灵活使用,不要过分追求重用性。
随着应用程序变得复杂和模块化,初始化过程和依赖管理变得越来越重要。Bootstrapper模式
在集中管理这些初始化逻辑方面非常有用。
虽然所有平台都以Application设计为基础,但由于各平台的特性和方式不同,Application设计各不相同。因此,为了在所有平台上保持相同的开发方式,使用Bootstrapper结构设计
是非常有效的。
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,使其在各种平台上以一致的方式运行。
通过在其他基于XAML的跨平台框架中最大限度地利用WPF中使用的技术和模式,可以提高开发效率。
Jamesnet.Core是基于.NET Standard 2.0
的框架,允许在WPF、Uno Platform和WinUI 3中实现相同的项目设计。该框架具有以下特点:
优点:
Uno Platform Desktop
可以在macOS和Linux上进行开发和运行。JetBrains Rider
构建跨平台开发环境。英雄联盟客户端重构项目利用Jamesnet.Core框架,在WPF、Uno Platform和WinUI 3等不同平台上使用相同的代码库和架构实现。
战略方法:
VisualStateManager(VSM)
替代Trigger,在不同平台上以相同方式管理UI状态。成果:
WPF技术和模式仍然强大,将其应用于跨平台开发可以提高开发效率和代码重用性。特别是使用Jamesnet.Core框架,通过DI容器的集中化管理策略和引入Bootstrapper,有助于降低视图和视图模型之间的耦合度,提高可维护性。
此外,通过积极使用VisualStateManager和IValueConverter,可以最小化平台之间的差异并保持一致的设计。通过这些策略,可以超越WPF基础,战略性地实现跨平台技术扩展。
特别是,UWP、WinUI 3和Uno Platform之间100%相同地使用XAML相关DLL,因此这些平台之间几乎没有差异。因此,对WPF开发者来说,使用Uno Platform桌面版非常有效且具有战略意义。这是因为从WPF转换到Uno可以在几小时内完成,转换到WinUI 3也非常容易。
未来,WPF技术和基于XAML的框架将继续发展,利用这些进行跨平台开发将变得更加重要。开发人员需要很好地把握这些趋势,制定适当的策略,开发高质量的应用程序。
参考资料
[1]
源码链接1: https://github.com/jamesnetgroup/leagueoflegends-wpf
[2]
源码链接2: https://github.com/jamesnetgroup/leagueoflegends-uno
[3]
源码链接3: https://github.com/jamesnetgroup/leagueoflegends-winui3
[4]
GitHub - jamesnetgroup: https://github.com/jamesnetgroup
[5]
GitHub - leagueoflegends-wpf: https://github.com/JamesnetGroup/leagueoflegends-wpf
[6]
GitHub - leagueoflegends-uno: https://github.com/JamesnetGroup/leagueoflegends-uno
[7]
GitHub - leagueoflegends-winui3: https://github.com/JamesnetGroup/leagueoflegends-winui3
[8]
GitHub - jamesnet.core: https://github.com/JamesnetGroup/leagueoflegends-wpf/tree/main/src/Jamesnet.Core
[9]
GitHub - leagueoflegends-wpf: https://github.com/JamesnetGroup/leagueoflegends-wpf
[10]
GitHub - leagueoflegends-uno: https://github.com/JamesnetGroup/leagueoflegends-uno
[11]
GitHub - leagueoflegends-winui3: https://github.com/JamesnetGroup/leagueoflegends-winui3
[12]
实现主题切换: https://www.bilibili.com/video/BV1FN41eHE7e
[13]
实现Riot PlayButton: https://www.bilibili.com/video/BV1Tu4y1j7Ei
[14]
实现导航栏: https://www.bilibili.com/video/BV1Ui4y1a717
[15]
实现Riot Slider: https://www.bilibili.com/video/BV1uy421a7yM
[16]
实现智能日期: https://www.bilibili.com/video/BV1pE421L7c2
[17]
实现Cupertino TreeView: https://www.bilibili.com/video/BV1xz42187wV