目录
介绍
用于Avalonia的Gidon IoC/MVVM框架
在本文中,我介绍了一个新的Gidon IoC/MVVM框架,该框架是在我自己的IoCy控制反转/依赖注入容器之上为一个出色的多平台类WPF包Avalonia构建的。据我所知,它是Avalonia的第一个IoC/MVVM框架,尽管我知道之前有一些尝试(不确定是否成功)为Avalonia移植Prism/MEF。
所以合理的问题是为什么不直接移植Prism(或使用它以前的移植)而不是构建一个新框架?
在我看来,它经常使用的Prism和MEF过于复杂,允许使用一些不应该与WPF和Avalonia一起使用的旧范式(例如,事件聚合)并且文档很少。
Gidon的目的是提供非常简单的API和实现,同时涵盖所有需要的功能。
请注意,Gidon框架已经非常可操作(如本文的示例所示)。在不久的将来,仍然会添加许多新的和很棒的功能。
模型-视图-视图模型(MVVM)模式的复习
什么是MVVM
MVVM模式由三部分组成:
- 模型——来自后端的非可视数据
- 视图模型——也是包含数据的非可视对象,但也提供非可视属性以反映可视按钮、菜单项等调用的可视功能和方法。
- 视图——表示应用程序视觉效果的视觉对象
View Model知道模型,但反之则不然。
视图是围绕视图模型构建的,因此它对它有一些了解,但视图模型不应该对视图一无所知。
MVVM:视图知道视图模型,视图模型反过来又知道模型,反之亦然
View通常是被动的——它只是模仿它的View Model并调用View Model的方法。View只知道它自己的View Model——不同View之间的所有通信通常都是通过它们各自的View Model完成的。
MVVM:视图之间的通信仅通过它们的视图模型完成
重要提示:View与其View Model之间的双向通信并不意味着View Model知道有关View的任何信息:从View Model到View的通信是通过绑定或事件来实现的。
MVVM模式的主要优点是视图的非常复杂的可视对象只是简单地模仿视图模型的更简单的非可视对象。非可视视图模型对象更容易创建、扩展、测试和调试,并且由于所有业务逻辑都位于视图模型中,因此MVVM应用程序变得更容易构建和维护。
MVVM模式最初是为WPF开发而发明的,因为WPF具有出色的绑定能力,但后来也被其他工具和框架采用。当然,每个XAML框架(包括Avalonia、UWP、Xamarin等)都支持MVVM,但Angular和Knockout JavaScript包本质上也是MVVM框架。
在您的代码中遵循MVVM模式,通常不需要任何控制反转或依赖注入。
重要提示:在我广泛的实践中,很少需要模型——后端数据可以直接反序列化到View Model类中。所以我主要在没有模型的情况下练习View-View Model (VVM)模式,但为了简单起见,我将这两种方法都称为MVVM。
要了解有关MVVM模式的更多信息,您可以阅读我的文章——简单示例中的多平台Avalonia .NET Framework编程高级概念。
用于MVVM的Avalonia工具
在Avalonia中将非可视视图模型转换为可视视图的最佳方法是使用ContentPresenter和ItemsPresenter控件(在WPF中,这将是相应的ContentControl和ItemsControl)。
ContentPresenter是将单个非可视对象转换为可视对象的理想方法,可以通过将传递给其Content属性的视图模型对象与传递给其ContenTemplate属性的DataTemplate “嫁接”来实现:
ItemsPresenter很适合将非可视视图模型对象的集合(存储在Items属性中)转换为可视视图模型对象的集合,方法是将存储在ItemTemplate属性中的DataTemplate应用于每个对象。结果集合中的视觉效果根据ItemsPresenter.ItemsPanel属性提供的Avalonia Panel进行排列(默认情况下,它们垂直堆叠,一个在下一个的顶部)。
控制反转(IoC)容器(无MVVM)的复习
MVVM和IoC不必同时使用。有许多简单的控制反转(插件)容器与MVVM或任何可视化框架无关。其中有:
- MEF
- Autofac
- Unity
- Ninject
- Castle Windsor
- IoCy——我自己的简单IoC容器可在IoCy中获得。
此类框架的主要目的是促进将功能拆分为松散耦合的插件(一些是静态加载的,一些是动态加载的),以改善应用程序中关注点的分离。
这将带来以下好处:
- 插件独立性——修改一个插件的实现不应触发其他插件的更改
- 更容易的可测试性和调试——应该能够轻松地单独测试、调试和修改每个插件(连同它所依赖的插件),并且固定插件应该与应用程序的其余部分一起工作,而无需对其他插件进行任何更改。
- 改进了产品的可扩展性——当您需要新功能时,您知道要为该特定扩展修改哪个插件,或者如果需要,您可以将新插件添加到现有插件中,仅在可能使用新API的地方进行修改。
我看到许多项目(不是我设计和启动的)只使用了上面列出的最后一个优势——通过向应用程序添加插件来扩展应用程序。另一方面,他们的插件如此混杂和相互依赖,以至于一个人无法在不影响其他插件的情况下取出其中一个。这是一个非常重要的错误——为了获得好的插件架构的好处,不同插件之间的相互依赖应该是最小的,架构师的任务是确保这种情况。
从某种意义上说,插件类似于硬件卡,而插件接口则很像插卡的插槽。
插件
接口
重要提示:更换、添加或删除插件应该像更换、添加或删除已经暴露的计算机中的计算机卡一样简单。如果不是这种情况,您的插件架构需要额外的工作。
测试插件应该像将卡放入硬件测试器中一样简单,发送一些输入并检查测试器中的相应输出。当然,首先应该为插件构建一个测试器。
但是,总的来说,软件插件比硬件卡具有以下优势:
- 在软件中生成一个插件对象的成本比在硬件中生成一个卡要小得多,因为使用多个相同类型的插件对象不会增加应用程序的成本。此外,保证相同类型的不同对象以相同的方式运行——软件缺陷是按类型而不是按对象。
- 插件可以是分层的,即插件本身可以由不同的子插件组成(硬件插件也可以有一些子插件,但在软件中,层次结构可以由所需的多个级别组成)。
- 一些插件可以是单例的——在许多不同的地方使用相同的插件(这在硬件中当然是不可能的)。
请注意,虽然插件层次结构没问题,但插件永远不应交叉依赖或对等依赖。这意味着如果插件是逻辑对等点,它们不应该相互依赖——一个通用功能应该被分解到不同的插件或非插件DLL中。
为什么要同时使用IoC和MVVM?
上面已经说过,MVVM可以在没有IoC的情况下实践,而IoC可以在没有MVVM的情况下实践,那么为什么我们需要一个可以同时做这两者的框架呢?原因是视图及其对应的视图模型是构建为插件的良好候选者。在这种情况下,每个开发人员都可以使用自己的视图/视图模型组合,与团队的其他成员分开测试它们,然后将它们作为插件组合在一起,理想情况下一切都会正常工作。
当然,有时视图模型并不是完全独立的——它们需要相互通信。通信机制可以通过称为服务的非可视单例插件连接,有时甚至可以内置到框架中。
有几个众所周知的IoC/MVVM框架通常围绕Microsoft的MEF或Unity IoC容器构建,有些甚至可以同时使用两者。所有这些最初都是为WPF创建的,但后来也适用于Xamarin和UWP。其中有:
- Prism
- Caliburn/Caliburn.Micro
- Cinch
IoCy容器的刷新
在这里,我将描述我的IoCy简单而强大的容器的功能。我在MEF、Autofac和Ninject中添加了我喜欢的所有功能,同时跳过了未广泛使用的功能。
IoC和DI(依赖注入)实现的主要原则是,可注入对象不是通过调用其构造函数来创建,而是通过调用容器上的某些方法来创建或查找要返回的对象。容器是在返回对象的正确实现之前创建的。每个可注入对象都可能具有一些也可注入的属性。在这种情况下,这些属性也是从同一个容器中填充的,以此类推。
这是IoCy最重要的功能:
创建容器
IoCContainer container = new IoCContainer();
您可以将唯一的容器名称传递给构造函数,否则,它将生成唯一的名称。
在接口(或超类)和实现(或子类)之间创建映射
container.Map<IPerson, Person>();
设置IPerson接口和Person实现IPerson的映射关系,这样每次
IPerson person = container.Resolve<IPerson>();
方法被调用,它将创建并返回一个新Person对象。
请注意,也可以为IPerson接口创建不同的映射,例如到类SuperPerson,但是为了使这两个映射同时存在,应该将一些对象作为映射传递id给Map(object id = null)方法,然后也将其相同的id传递给相应的Resolve(object id = null)方法:
container.Map<IPerson, Person>(1);
IPerson superPerson = container.Resolve<IPerson>(1);
在上面的代码中,id是一个整数并且等于1。
请注意,所有其他映射和解析方法也接受id参数。
在接口(或超类)和实现(或子类)之间创建单例映射
与前一种情况不同,每次Resolve<...>()方法调用都会获得一个新创建的对象,单例映射每次都会返回相同的对象。
以下是您如何进行单例映射:
container.MapSingleton<ILog, FileLog>();
解析Singleton和之前完全一样,只是每次都返回同一个对象。
还有另一种设置单顶映射的方法,如果你想要一个已经存在的对象是Singleton,你只需将它传递给MapSingleton(...)方法:
ConsoleLog consoleLog = new ConsoleLog();
// change the mapping of ILog to ConsoleLog (instead of FileLog)
childContainer.MapSingleton<ILog, ConsoleLog>(consoleLog);
创建多重映射
多重映射创建映射到键的特定类型项目的集合。每次调用MapMultiType(...)它都会向集合中添加一个项。例如:
container.MapMultiType<ILog, FileLog>();
container.MapMultiType<ILog, ConsoleLog>();
将向容器内部集合添加两个对象,一个是类型FileLog,另一个是类型ConsoleLog。相应地,在容器上调用MultiResolve()方法将这两个项集合作为IEnumerable<ILog>返回:
IEnumerable<ILog> logs = container.MultiResolve<ILog>();
使用属性进行合成
与MEF中相同,IoCy允许使用属性在容器内组合对象。例如:
[Implements]
public class Person : IPerson
{
public string PersonName { get; set; }
[Part]
public IAddress Address { get; set; }
}
顶部的[Implements]属性意味着此实现映射到某种类型。由于它映射到的确切类型没有指定为属性的参数,默认情况下,它映射到当前类的基类(如果基类不是object);如果基类是object,它映射到该类实现的第一个接口。由于我们的Person类没有基类,它将映射到第一个接口(IPerson)。所以上面的代码相当于container.Map<IPerson, Person>();。但是,建议您将要映射到的类作为第一个参数TypeToResolve传递给属性,这样对类的更改(例如,更改类实现的接口的顺序)不会影响组合。
以下属性声明:[Implements(typeof(IPerson))], 比上面使用的要好。
[Part]属性上面的Address属性意味着该Address对象也是可注入的(来自容器)。请注意,注入的属性不知道它注入的对象是单例还是每次都重新创建——这取决于容器如何填充它。
对于Multi实现,应该使用类上方的[MultiImplements(...)]属性和属性上方的[MultiPart]属性,例如,
[MultiImplements(typeof(IPlugin))]
public class PluginOne : IPlugin
{
public void PrintMessage()
{
Console.WriteLine("I am PluginOne!!!");
}
}
[MultiImplements(typeof(IPlugin))]
public class PluginTwo : IPlugin
{
public void PrintMessage()
{
Console.WriteLine("I am PluginTwo!!!");
}
}
[Implements(typeof(IPluginAccumulator))]
public class PluginAccumulator : IPluginAccumulator
{
[MultiPart]
public IEnumerable<IPlugin> Plugins { get; set; }
}
相应地,有几种IoCContainer方法允许从静态或动态加载的整个程序集,甚至从位于某个路径下的所有DLL程序集组成容器。这是列表:
public class IoCContainer
{
...
// injects an already loaded assembly
public void InjectAssembly(Assembly assembly){...}
// loads and injects a dynamic assembly by path to its dll
public void InjectDynamicAssemblyByFullPath(string assemblyPath){...}
// loads and injects all dll files located at assemblyFolderPath
// whose name matches the regex
public void InjectPluginsFromFolder
(string assemblyFolderPath, Regex? matchingFileName = null){...}
// loads and injects assemblies that match the rejex
// from all direct sub-folders of folder specified
// by baseFolderPath argument.
public void InjectPluginsFromSubFolders
(string baseFolderPath, Regex? matchingFileName = null){...}
}
Gidon示例
代码位置
此时,为了运行Gidon示例,您必须从Gidon下载整个Gidon代码。
为此,您应该使用带有递归子模块的git命令:
git clone https://github.com/npolyak/NP.Avalonia.Gidon.git --recursive NP.Avalonia.Gidon
或者,如果您在克隆期间忘记了用户' --recursive'选项,则始终可以在克隆后在存储库中使用以下命令:
git submodule update --init
以下是存储库基本目录的子文件夹:
- Prototypes——包含Gidon的样本,其中一些将在下面的文章中讨论
- Src——包含Gidon的代码
- SubModules ——包含来自其他存储库的代码作为Gidon依赖的子模块
- Tests——包含跨多个原型使用的代码。特别是,我在这里放置了测试插件和服务。
插件测试
解决方案位置和结构
PluginsTest解决方案位于Prototypes/PluginsTest文件夹下。打开解决方案(为此您需要VS2022)。
使PluginsTest项目成为解决方案的启动项目。
这是解决方案文件夹/项目结构:
右键单击PluginsTest项目并选择Rebuild。请注意,插件是动态加载到应用程序中的,主项目并不直接依赖它们。为了构建所有插件,您必须右键单击解决方案中的“TestAndMocks”解决方案文件夹并选择Rebuild。
插件/服务项目将被重新构建,它们的编译程序集将被复制(通过构建后事件)到当前解决方案下的bin/Debug/net6.0文件夹中。从该文件夹中,插件将由Gidon框架动态加载。这是安装在<CurrentSolution>/bin/Debug/net6.0下的插件的文件夹结构:
运行PluginsTest项目
尝试运行应用程序,您应该看到以下内容:
按“退出”按钮将退出应用程序。
用户“nick”为用户名,“1234”为密码。按“登录”按钮(它应该已启用),您将看到以下内容:
这是两个可停靠/浮动窗格,您可以将它们的标题拉出主窗口。它们之间通过服务建立连接——如果您在“输入文本” TextBox中键入任何内容并按下按钮“发送”(它将启用),测试将出现在另一个可停靠窗格中:
主项目PluginsTest的代码
守则解释
现在让我们看一下代码。
加载插件的Gidon代码
加载插件的代码位于App构造函数内的App.asaml.cs文件中:
public class App : Application
{
/// defined the Gidon plugin manager
/// use the following paths (relative to the PluginsTest.exe executable)
/// to dynamically load the plugins and services:
/// "Plugins/Services" - to load the services (non-visual singletons)
/// "Plugins/ViewModelPlugins" - to load view model plugins
/// "Plugins/ViewPlugins" - to load view plugins
public static PluginManager ThePluginManager { get; } =
new PluginManager
(
"Plugins/Services",
"Plugins/ViewModelPlugins",
"Plugins/ViewPlugins");
// expose the IoC container
public static IoCContainer TheContainer => ThePluginManager.TheContainer;
public App()
{
// inject a type from a statically loaded project NLogAdapter
ThePluginManager.InjectType(typeof(NLogWrapper));
// inject all dynamically loaded assemblies
ThePluginManager.CompleteConfiguration();
}
...
}
App.axaml包括来自默认主题的样式以及UniDock框架的样式以工作:
<Application.Styles>
<StyleInclude Source="avares://Avalonia.Themes.Default/Accents/BaseLight.xaml"/>
<StyleInclude Source="avares://Avalonia.Themes.Default/DefaultTheme.xaml"/>
<StyleInclude Source="avares://NP.Avalonia.Visuals/Themes/CustomWindowStyles.axaml"/>
<StyleInclude Source="avares://NP.Avalonia.UniDock/Themes/DockStyles.axaml"/>
</Application.Styles>
最有趣的代码位于MainWindow.axaml文件中。
首先,我们需要在Window标签中定义一些XML命名空间:
<Window ...
xmlns:utils="clr-namespace:NP.Utilities.PluginUtils;assembly=NP.Utilities"
xmlns:basicServices="clr-namespace:NP.Utilities.BasicServices;assembly=NP.Utilities"
xmlns:np="https://np.com/visuals"
xmlns:local="clr-namespace:PluginsTest"
...
>
XML命名空间np:是最重要的命名空间——用于所有与Avalonia相关的功能,包括Gidon的代码。
然后为了使用UniDock框架,我们需要将DockManager定义为Avalonia XAML资源:
<Window.Resources>
<np:DockManager x:Key="TheDockManager"/>
</Window.Resources>
然后,我们有一个Grid面板,一个用于显示身份验证插件的PluginControl,一个Grid用于显示包含其他插件的可停靠面板,用于发送和显示一些文本:
<Grid>
<np:PluginControl x:Name="AuthenticationPluginControl"
TheContainer="{x:Static local:App.TheContainer}">
...
</np:PluginControl>
<Grid x:Name="DockContainer"
.../>
</Grid>
一次只能看到其中一项:如果用户未通过身份验证,则身份验证PluginControl可见,否则包含用于发送和显示文本的插件的可停靠面板可见。
让我们首先关注Authentication PluginControl:
<np:PluginControl x:Name="AuthenticationPluginControl"
TheContainer="{x:Static local:App.TheContainer}">
<np:PluginControl.PluginInfo>
<utils:VisualPluginInfo ViewModelType="{x:Type utils:IPlugin}"
ViewModelKey="AuthenticationVM"
ViewDataTemplateResourcePath=
"avares://AuthenticationViewPlugin/Views/AuthenticationView.axaml"
ViewDataTemplateResourceKey="AuthenticationViewDataTemplate"/>
</np:PluginControl.PluginInfo>
</np:PluginControl>
PluginControl来自Gidon框架。它派生自ContentPresenter(同样的ContentPresenter,将一个视图模型转换为一个视图,正如我们上面解释的那样)。除了派生功能之外,PluginControl还定义了几个有用的Styled属性(Avalonia中的Styled属性与WPF中的依赖属性非常相似):
- TheContainer Styled属性允许指定IoCContainer,其PluginControl需要从中获取其视图模型和视图插件(以及它们所依赖的所有插件)。
- PluginInfo Styled属性允许传递指定如何从容器中检索视图模型对象(用于填充PluginControl.Content属性)和View对象(用于填充PluginControl.ContentTemplate属性)的信息。PluginInfo是在NP.Utilities包中定义的VisualPluginInfo类型。
我们的身份验证插件控件上的App.TheContainer static属性通过x:Static标记扩展连接到该TheContainer属性。
我们VisualPluginInfo对象的前两个属性ViewModelType和ViewModelKey用于检索视图模型插件。ViewModelType等于typeof(IPlugin)。因为IPlugin很常见(几乎每个视图模型插件都实现了),所以我们还使用ViewModelKey设置为"AuthenticationVM"字符串来明确标识身份验证视图模型单例插件。
以下是在相应的AuthenticationViewModelPlugin项目中定义AuthenticationViewModel插件的方式:
[Implements(typeof(IPlugin), partKey:"AuthenticationVM", isSingleton:true)]
public class AuthenticationViewModel : VMBase, IPlugin
{
...
}
VisualPluginInfo的最后两个属性用于指定视图(在Gidon的示例中应该只是一个DataTemplate。
属性ViewDataTemplateResourcePath指定包含 DataTemplate(在我们的例子中,它是“avares://AuthenticationViewPlugin/Views/AuthenticationView.axaml”)的XAML资源文件的URL。属性ViewDataTemplateResourceKey指定View DataTemplate的资源键(在我们的例子中,它是“AuthenticationViewDataTemplate”)。实际上,您可以检查位于AuthenticationViewPlugin项目“Views”项目文件夹中的文件“AuthenticationView.asaml” ,您将看到那里定义的“AuthenticationViewDataTemplate”。
我们的身份验证PluginControl的可见性是通过它的View管理的(从某种意义上说,如果用户通过身份验证,PluginControl内部的任何东西都是不可见的,而不是PluginControl本身。
现在来看看<Grid x:Name="DockContainer" ... />。它包含了带有两个DockItems的UniDock对接层次结构——一个在左侧包含用于输入和发送文本的视图模型/视图插件,一个在右侧用于显示已发送的文本:
<Grid x:Name="DockContainer"
IsVisible="{Binding Path=(np:PluginAttachedProperties.PluginDataContext).IsAuthenticated,
RelativeSource={RelativeSource Self}}"
np:PluginAttachedProperties.TheContainer="{x:Static local:App.TheContainer}">
<np:PluginAttachedProperties.PluginVmInfo>
<utils:ViewModelPluginInfo ViewModelType=
"{x:Type basicServices:IAuthenticationService}"/>
</np:PluginAttachedProperties.PluginVmInfo>
<np:RootDockGroup TheDockManager="{StaticResource TheDockManager}">
<np:StackDockGroup TheOrientation="Horizontal">
<np:DockItem Header="Enter Text">
<np:PluginControl x:Name="EnterTextPluginControl"
TheContainer="{x:Static local:App.TheContainer}">
<np:PluginControl.PluginInfo>
<utils:VisualPluginInfo ViewModelType="{x:Type utils:IPlugin}"
ViewModelKey="EnterTextViewModel"
ViewDataTemplateResourcePath=
"avares://EnterTextViewPlugin/Views/EnterTextView.axaml"
ViewDataTemplateResourceKey="EnterTextView"/>
</np:PluginControl.PluginInfo>
</np:PluginControl>
</np:DockItem>
<np:DockItem Header="Received Text">
<np:PluginControl x:Name="ReceiveTextPluginControl"
TheContainer="{x:Static local:App.TheContainer}">
<np:PluginControl.PluginInfo>
<utils:VisualPluginInfo ViewModelType="{x:Type utils:IPlugin}"
ViewModelKey="ReceiveTextViewModel"
ViewDataTemplateResourcePath=
"avares://ReceiveTextViewPlugin/Views/ReceiveTextView.axaml"
ViewDataTemplateResourceKey="ReceiveTextView"/>
</np:PluginControl.PluginInfo>
</np:PluginControl>
</np:DockItem>
</np:StackDockGroup>
</np:RootDockGroup>
</Grid>
DockItems中的PluginControls设置方式与设置身份验证PluginControl的方式非常相似,只是它们指向不同的View Model和View插件,因此我们不打算在这里花时间讨论它们(尽管我们将在下面解释这些插件)。RootDockGroup,StackDockGroup和DockItem是UniDock框架对象:
- RootDockGroup是每个停靠层次结构顶部的停靠组。
- StackDockGroup垂直或水平排列它的孩子(在我们的例子中水平排列,因为它的TheOrientantion属性设置为Horizontal。
- DockItem实际上是带有标题和内容的停靠/浮动窗格。
关于我们的<Grid x:Name="DockContainer" ...>面板,我们需要解释的是我们如何根据用户是否经过身份验证来切换其可见性。以下是相关代码:
<Grid x:Name="DockContainer"
IsVisible="{Binding Path=(np:PluginAttachedProperties.PluginDataContext).IsAuthenticated,
RelativeSource={RelativeSource Self}}"
np:PluginAttachedProperties.TheContainer="{x:Static local:App.TheContainer}">
<np:PluginAttachedProperties.PluginVmInfo>
<utils:ViewModelPluginInfo ViewModelType=
"{x:Type basicServices:IAuthenticationService}"/>
</np:PluginAttachedProperties.PluginVmInfo>
...
</Grid>
在这里,我们依赖于NP.Avalonia.Gidon项目中PluginAttachedProperties类中定义的Avalonia附加属性。我们使用:Static标记扩展将PluginAttachedProperties.TheContainer附加属性设置为我们的App.TheContainer staticx属性:
np:PluginAttachedProperties.TheContainer="{x:Static local:App.TheContainer}"
然后我们将附加属性PluginAttachedProperties.PluginVmInfo设置为:
<utils:ViewModelPluginInfo ViewModelType="{x:Type basicServices:IAuthenticationService}"/>
ViewModelPluginInfo仅包含VisualPluginInfo的视图模型部分。在我们的例子中,我们正在检索其是单例类型MockAuthenticationService的IAuthenticationService的实现(我们不需要ViewModelKey,因为在我们的容器中只有一个IAuthenticationService类型的对象)。
一旦在我们的Grid上设置了两个附加属性,相同Grid的PluginAttachedProperties.PluginDataContext附加属性将被设置为包含从容器中检索到的类型IAuthenticationService的对象。此对象具有带有更改通知的IsAuthenticated属性(属性更改时触发INotifyPropertyChanged.PropertyChanged事件)。
现在我们所需要做的就是将Grid上的IsVisible属性绑定到PluginAttachedProperties.PluginDataContext属性所包含的对象上定义的IsAuthenticated属性的路径:
<Grid x:Name="DockContainer"
IsVisible="{Binding Path=(np:PluginAttachedProperties.PluginDataContext).IsAuthenticated,
RelativeSource={RelativeSource Self}}"
...
>
身份验证插件和服务
身份验证视图模型插件
身份验证视图模型插件在AuthenticationViewModelPlugin项目中定义:
[Implements(typeof(IPlugin), partKey: "AuthenticationVM", isSingleton: true)]
public class AuthenticationViewModel : VMBase, IPlugin
{
[Part(typeof(IAuthenticationService))]
// Authentication service that comes from the container
public IAuthenticationService? TheAuthenticationService
{
get;
private set;
}
...
// notifiable property
public string? UserName { get {...} set {...} }
...
// notifiable property
public string? Password { get {...} set {...} }
// change notification fires when either UserName or Password change
public bool CanAuthenticate =>
(!string.IsNullOrEmpty(UserName)) && (!string.IsNullOrEmpty(Password));
// method to call in order to try to authenticate a user
public void Authenticate()
{
TheAuthenticationService?.Authenticate(UserName, Password);
OnPropertyChanged(nameof(IsAuthenticated));
}
// method to exit the application
public void ExitApplication()
{
Environment.Exit(0);
}
// IsAuthenticated property
// whose change notification fires within Authenticate() method
public bool IsAuthenticated => TheAuthenticationService?.IsAuthenticated ?? false;
}
模拟身份验证服务
Authentication View Model Plugin使用的IAuthenticationService,在MockAuthentication项目中实现为MockAuthenticationService,也很简单:
[Implements(typeof(IAuthenticationService), IsSingleton = true)]
public class MockAuthenticationService : VMBase, IAuthenticationService
{
...
// notifiable property
public string? CurrentUserName { get {...} set {...} }
// Is authenticated is true if and only if the CurrentUserName is not zero
public bool IsAuthenticated => CurrentUserName != null;
// will only authenticate if userName="nick" and password="1234"
public bool Authenticate(string userName, string password)
{
if (IsAuthenticated)
{
throw new Exception("Already Authenticated");
}
CurrentUserName =
(userName == "nick" && password == "1234") ? userName : null;
...
return IsAuthenticated;
}
public void Logout()
{
if (!IsAuthenticated)
{
throw new Exception("Already logged out");
}
CurrentUserName = null;
}
}
认证视图
如上所述,Gidon中的视图应定义为DataTemplates。身份验证视图被定义为AuthenticationViewPlugin项目Views/AuthenticationView.axaml文件中的DataTemplate资源:
<DataTemplate x:Key="AuthenticationViewDataTemplate">
<Grid Background="{DynamicResource WindowBackgroundBrush}"
RowDefinitions="*, Auto"
IsVisible="{Binding Path=IsAuthenticated,
Converter={x:Static np:BoolConverters.Not}}">
<Control.Styles>
<StyleInclude Source="avares://NP.Avalonia.Visuals/Themes/ThemeStyles.axaml"/>
</Control.Styles>
<StackPanel HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="10">
<np:LabeledControl x:Name="EnterUserNameControl"
Text="Enter User Name: "
Classes="Bla"
HorizontalAlignment="Center">
<np:LabeledControl.ContainedControlTemplate>
<ControlTemplate>
<TextBox Width="150"
Text="{Binding Path=UserName, Mode=TwoWay}"/>
</ControlTemplate>
</np:LabeledControl.ContainedControlTemplate>
</np:LabeledControl>
<np:LabeledControl x:Name="EnterPasswordControl"
Text="Enter Password: "
HorizontalAlignment="Center"
Margin="0,15,0,0">
<np:LabeledControl.ContainedControlTemplate>
<ControlTemplate>
<TextBox Width="150"
Text="{Binding Path=Password, Mode=TwoWay}"/>
</ControlTemplate>
</np:LabeledControl.ContainedControlTemplate>
</np:LabeledControl>
</StackPanel>
<StackPanel Orientation="Horizontal"
Margin="10"
Grid.Row="1"
HorizontalAlignment="Right"
VerticalAlignment="Center">
<Button Content="Exit"
np:CallAction.TheEvent="{x:Static Button.ClickEvent}"
np:CallAction.MethodName="ExitApplication"/>
<Button Content="Login"
Margin="10,0,0,0"
IsEnabled="{Binding Path=CanAuthenticate}"
np:CallAction.TheEvent="{x:Static Button.ClickEvent}"
np:CallAction.MethodName="Authenticate"/>
</StackPanel>
</Grid>
</DataTemplate>
它有两个LabeledControl对象,一个在另一个之上排列——一个用于输入用户名,另一个用于输入密码。它们TextBoxes内部的Text属性是双向绑定的,UserName和Password string在View Model插件上定义。
单击按钮时, “退出”和“登录”按钮正在使用NP.Avalonia.Visuals项目中的CallAction行为来相应地调用ExitApplication()和Authenticate()视图模型方法。
输入和接收文本插件和服务
文本服务
输入和接收文本视图模型插件通过实现ITextService接口的TextService相互通信:
[Implements(typeof(ITextService), IsSingleton = true)]
public class TextService : ITextService
{
public event Action<string>? SentTextEvent;
public void Send(string text)
{
SentTextEvent?.Invoke(text);
}
}
它的实现非常简单——它有一种方法Send(string text)可以触发SendTextEvent将文本传递给它。输入文本视图模型调用该Send(string text)方法,接收文本视图模型处理SentTextEvent获取文本并将其分配给它自己的通知Text属性。
输入文本视图模型插件
这个插件由一个简单的类组成——EnterTextViewModel:
[Implements(typeof(IPlugin), partKey: nameof(EnterTextViewModel), isSingleton: true)]
public class EnterTextViewModel : VMBase, IPlugin
{
// ITextService implementation
[Part(typeof(ITextService))]
public ITextService? TheTextService { get; private set; }
#region Text Property
private string? _text;
// notifiable property with getter and setter
public string? Text { ... }
#endregion Text Property
// change notified the Text changes
public bool CanSendText => !string.IsNullOrWhiteSpace(this._text);
// method to send the text via TextService
public void SendText()
{
if (!CanSendText)
{
throw new Exception("Cannot send text, this method should not have been called.");
}
TheTextService!.Send(Text!);
}
}
输入文本视图插件
该插件位于EnterTextViewPlugin项目的Views/EnterTextView.asaml文件中:
<DataTemplate x:Key="EnterTextView">
<Grid RowDefinitions="*, Auto">
<Control.Styles>
<StyleInclude Source="avares://NP.Avalonia.Visuals/Themes/ThemeStyles.axaml"/>
</Control.Styles>
<np:LabeledControl Text="Enter Text: ">
<ControlTemplate>
<TextBox Text="{Binding Path=Text, Mode=TwoWay}"
Width="150"/>
</ControlTemplate>
</np:LabeledControl>
<Button Content="Send"
Grid.Row="1"
IsEnabled="{Binding Path=CanSendText}"
np:CallAction.TheEvent="{x:Static Button.ClickEvent}"
np:CallAction.MethodName="SendText"
...
/>
</Grid>
</DataTemplate>
有一种TextBox用于输入文本的两种方式绑定到视图模型上的Text属性。单击视图模型上还有一个用于调用SentText()方法的按钮。
接收文本视图模型插件
位于ReceiveTextViewModel项目:
[Implements(typeof(IPlugin), partKey: nameof(ReceiveTextViewModel), isSingleton: true)]
public class ReceiveTextViewModel : VMBase, IPlugin
{
ITextService? _textService;
// ITextService implementation
[Part(typeof(ITextService))]
public ITextService? TheTextService
{
get => _textService;
private set
{
if (_textService == value)
return;
if (_textService != null)
{
// disconnect old service's SentTextEvent
_textService.SentTextEvent -= _textService_SentTextEvent;
}
_textService = value;
if (_textService != null)
{ // connect the handler to the service's
// SentTextEvent
_textService.SentTextEvent += _textService_SentTextEvent;
}
}
}
// set Text property when receives it from TheTextService
// via SentTextEvent
private void _textService_SentTextEvent(string text)
{
Text = text;
}
#region Text Property
private string? _text;
// notifiable property
public string? Text { get {...} private set {...} }
#endregion Text Property
}
接收文本视图插件
<DataTemplate x:Key="ReceiveTextView">
<Grid>
<Control.Styles>
<StyleInclude Source="avares://NP.Avalonia.Visuals/Themes/ThemeStyles.axaml"/>
</Control.Styles>
<np:LabeledControl Text="The Received Text is:"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="10">
<ControlTemplate>
<TextBlock Text="{Binding Path=Text, Mode=OneWay}"
FontWeight="Bold"/>
</ControlTemplate>
</np:LabeledControl>
</Grid>
</DataTemplate>
本质上——它只包含一个TextBox,其Text属性双向绑定到视图模型上的Text属性。
使用Gidon框架实践原型驱动开发
我最近在原型驱动开发(PDD)文章中描述了原型驱动开发或PDD。
这是一种开发类型,您首先创建一个包含您需要的功能的原型。然后将可重用功能从该原型移至通用项目,最后在您的主应用程序项目中使用该功能:
插件架构非常适合PDD。实际上,主项目通常不是静态依赖于插件(而是动态加载)。这便于运行时的灵活性,但不利于开发。
查看位于Prototypes/AuthenticationPluginTest文件夹下的AuthenticationPluginTest.sln解决方案。
它仅包含Authentication相关的插件和MockAuthentication服务:
但现在主要项目依赖于AuthenticationViewPlugin,AuthenticationViewModelPlugin和MockAuthentication项目。
这将允许您只重新编译一次,而不是分别重新编译插件和主项目。此外,这将使项目更轻松,因为您将只处理三个插件(视图模型、视图和服务),而不是应用程序中的所有插件)。此外,它将避免处理可能的错误,因为某些动态程序集在您期望它们更改时并没有更改。
一般来说,在PDD之后,可以首先在原型的主项目中创建身份验证插件功能。然后将视图模型移动到AuthenticationViewModelPlugin,并将视图移动到原型的主项目依赖的AuthenticationViewPlugin项目。
最后,在完善功能并确保其正常工作后,您可以将插件程序集设置为复制到插件文件夹中,并在另一个原型或主应用程序中将它们作为动态加载的插件进行测试。
此外,当您需要修改或调试插件时,您将能够在静态加载插件的原型中进行,并且对于动态加载插件的项目,修改将自动生效。
https://www.codeproject.com/Articles/5325733/Gidon-Avalonia-based-MVVM-Plugin-IoC-Container