首页 > 其他分享 >WPF开发框架Caliburn.Micro详解

WPF开发框架Caliburn.Micro详解

时间:2024-12-16 12:09:17浏览次数:4  
标签:eventAggregator ViewModel public Micro WPF Caliburn View

随着项目的发展,功能越来越复杂,解耦已经是每一个项目都会遇到的问题。在WPF开发中,MVVM开发模式是主要就是为了将UI页面和业务逻辑分离开来,从而便于测试,提升开发效率。当前比较流行的MVVM框架,主要有Prism,Community.Toolkit,以及今天介绍的Caliburn.Micro。而Caliburn.Micro框架是一款小巧但非常给力的框架,使用此框架,可以非常快速的构建WPF程序来支持MVVM开发模型。本文仅供学习分享使用,如有不足之处,还请指正。

Caliburn.Micro框架安装

首先打开VisualStudio开发工具,在WPF应用程序中,在项目或解决方案右键打开Nuget包管理器窗口,然后搜索Caliburn.Micro,然后进行安装即可。如下图所示: 

Caliburn.Micro基础配置

在新创建的WPF应用程序中,基础配置如下:

删除默认文件:WPF项目创建成功后,要使用Caliburn.Micro库,可以删除项目中默认创建的启动窗口MainWindow.xaml,并删除App.xaml中默认启动项StartupUri。

<Application x:Class="DemoCaliburn.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:DemoCaliburn">
    <Application.Resources>
        <ResourceDictionary>
        </ResourceDictionary>
    </Application.Resources>
</Application>

创建默认ViewModel,并命名为ShellViewModel,以及ShellView.xaml。ShellViewModel继承自Conductor或者PropertyChangedBase表示此类具备了属性通知功能。 ShellViewModel代码如下所示:

public class ShellViewModel : PropertyChangedBase
{
	string name;

	public string Name
	{
		get { return name; }
		set
		{
			name = value;
			NotifyOfPropertyChange(() => Name);
			NotifyOfPropertyChange(() => CanSayHello);
		}
	}

	public bool CanSayHello
	{
		get { return !string.IsNullOrWhiteSpace(Name); }
	}

	public void SayHello()
	{
		MessageBox.Show(string.Format("Hello {0}!", Name)); //Don't do this in real life :)
	}
}

创建Bootstrapper类,此类是配置Caliburn框架的核心,告诉程序改如何启动应用程序。在此类中重写OnStartUp函数,并在此方法中通过DisplayRootViewForAsync指定程序启动项ShellViewModel。同样也可以通过设置启动页面视图的方式Application.RootVisual = new ShellView();来启动程序。

public class Bootstrapper : BootstrapperBase
{
	public Bootstrapper()
	{
		Initialize();
	}

	protected override async void OnStartup(object sender, StartupEventArgs e)
	{
		await DisplayRootViewForAsync(typeof(ShellViewModel));
	}
}

在App.xaml中定义Bootstrapper对象,作为资源引用,而剩下的工作将由Bootstrapper完成。

<Application x:Class="DemoCaliburn.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:DemoCaliburn">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary>
                    <local:Bootstrapper x:Key="Bootstrapper" />
                </ResourceDictionary>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>

Caliburn.Micro采用了一种简单的命名约定【将ViewModel去掉Model就是View的名称,如:ShellViewModel对应的视图为ShellView】来为ViewModel指定需要渲染的View。 同样Caliburn.Micro框架还会根据元素的x:Name属性,在ViewModel中为控件匹配对应的属性或方法绑定。 

配置Bootstrapper,Bootstrapper类可以用于配置Caliburn.Micro的基础配置,如配置Ioc容器,事件聚合器,注入实例等 在Bootstrapper中,通过SimpleContainer注入和获取实例,以及Build实例 通过重写Configure方法,将要注入的对象注入到容器中。获取的时候通过类型和key进行获取 。

public class Bootstrapper:BootstrapperBase
{
	private readonly SimpleContainer _container = new SimpleContainer();

	public Bootstrapper()
	{
		Initialize();
		LogManager.GetLog = type => new DebugLog(type);//增加日志
	}

	protected override async void OnStartup(object sender, StartupEventArgs e)
	{
		await DisplayRootViewForAsync(typeof(ShellViewModel));
	}

	protected override void Configure()
	{
		_container.Instance(_container);
		_container.Singleton<IWindowManager, WindowManager>()
.Singleton<IEventAggregator, EventAggregator>();

		foreach (var assembly in SelectAssemblies())
		{
			assembly.GetTypes()
		   .Where(type => type.IsClass)
		   .Where(type => type.Name.EndsWith("ViewModel"))
		   .ToList()
		   .ForEach(viewModelType => _container.RegisterPerRequest(
			   viewModelType, viewModelType.ToString(), viewModelType));
		}
	}

	protected override IEnumerable<Assembly> SelectAssemblies()
	{
		// https://www.jerriepelser.com/blog/split-views-and-viewmodels-in-caliburn-micro/

		var assemblies = base.SelectAssemblies().ToList();
		//assemblies.Add(typeof(LoggingViewModel).GetTypeInfo().Assembly);
		return assemblies;
	}

	protected override object GetInstance(Type service, string key)
	{
		return _container.GetInstance(service, key);
	}

	protected override IEnumerable<object> GetAllInstances(Type service)
	{
		return _container.GetAllInstances(service);
	}

	protected override void BuildUp(object instance)
	{
		_container.BuildUp(instance);
	}
}

Action

在Caliburn.Micro中,Action通过Microsoft.Xaml.Behaviors的事件触发机制来实现。也就是说你可以使用任何Microsoft.Xaml.Behaviors.TriggerBase的派生类,来触发ActionMessage。

<UserControl x:Class="Caliburn.Micro.Hello.ShellView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
             xmlns:cal="http://www.caliburnproject.org">
    <StackPanel>
        <Label Content="Hello please write your name" />
        <TextBox x:Name="Name" />
        <Button Content="Click Me">
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="Click">
                    <cal:ActionMessage MethodName="SayHello" />
                </i:EventTrigger>
            </i:Interaction.Triggers>
        </Button>
    </StackPanel>
</UserControl> 

ActionMessage会沿着可视化树通过Bubbles的方式一层一层向上寻找能够处理的Target实例。如果寻找到Target,则响应事件,如果没有,则抛出异常。

默认情况下,任何通过ViewModelBinder来进行绑定的将会自动设置为Target对象,当然也可以通过附加属性Action.Target来为Action设置目标实例。

Message.Attach,此属性有一组简单的分析程序提供支持,它通过解析文本输入的方式来提供完整的ActionMessage。默认情况下,Message.Attach 声明未指定应由哪个事件发送消息。如果省略该事件,解析器将使用 ConventionManager 来确定要用于触发器的默认事件。对于 Button,它是 Click。 

<Button Content="Remove" cal:Message.Attach="[Event Click] = [Action Remove($dataContext)]" />
<Button Content="Click Me" cal:Message.Attach="[Event Click] = [Action SayHello(Name.Text)]" />
<!--使用智能默认值-->
<Button Content="Click Me" cal:Message.Attach="SayHello(Name)" />
<Button Content="Let's Talk" cal:Message.Attach="[Event MouseEnter] = [Action Talk('Hello', Name.Text)]; [Event MouseLeave] = [Action Talk('Goodbye', Name.Text)]" />

设置Action Target的几种方式:

  • Action.Target,通过设置Action.Target和DataContext属性来指定实例,以字符串的形式设置,Caliburn.Micro框架会自动通过Ioc容器进行解析。
  • Action.TargetWithoutContext,仅设置Action.Target来指定实例,同样以字符串的形式设置,Caliburn.Micro框架会自动通过Ioc容器进行解析。
  • Bind.Model,View-First方式,设置Action.Target和DataContext来指定实例,同样以字符串的形式设置,Caliburn.Micro框架会自动通过Ioc容器进行解析。
  • Bind.ModelWithoutContext,View-First方式,设置Action.Target来指定实例。
  • View.Model,ViewModel-First 优先的方式,为ViewModel指定对应的视图  

在ViewModel中,假如我们的Action需要一个参数name。

public class ShellViewModel : IShell
{
    public bool CanSayHello(string name)
    {
        return !string.IsNullOrWhiteSpace(name);
    }

    public void SayHello(string name)
    {
        MessageBox.Show(string.Format("Hello {0}!", name));
    }
} 

在UI视图中,可以通过Caliburn框架中的Parameter对象为ActionMessage指定参数。 Parameter对象的Value属性是依赖属性,可以进行Binding具备变更通知的属性。 可以为ActionMessage指定多个参数。

<UserControl x:Class="Caliburn.Micro.HelloParameters.ShellView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
             xmlns:cal="http://www.caliburnproject.org">
    <StackPanel>
        <TextBox x:Name="Name" />
        <Button Content="Click Me">
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="Click">
                    <cal:ActionMessage MethodName="SayHello">
                        <cal:Parameter Value="{Binding ElementName=Name, Path=Text}" />
                    </cal:ActionMessage>
                </i:EventTrigger>
            </i:Interaction.Triggers>
        </Button>
    </StackPanel>
</UserControl> 

通过上述方式,还可以有其他几种方式来为ActionMessage指定参数:

  • $eventArgs,通过EventArgs或输入参数为Action指定参数。
  • $dataContext,通过控件元素的DataContext指定参数。
  • $source,触发控件的实际FrameworkElement元素。
  • $view,绑定ViewModel的视图,一般是UserControl或Window。
  • $executionContext,Action执行的上下文,它包含的信息更多。
  • $this,UI控件绑定元素本身,将通过参数的形式将自身传递给Action。 

View-First

Caliburn.Micro默认采用ViewModel优先的方式加载程序,通过也可以通过View-First的方式启动。 在Bootstrapper的OnStartup方法中,通过Application.RootVisual = new ShellView();指定要启动的视图。

protected override void OnStartup(object sender, StartupEventArgs e)
{
	Application.RootVisual = new ShellView();
}

将ViewModel设置要导出的名称Key。

[Export("Shell", typeof(IShell))]
public class ShellViewModel : PropertyChangedBase, IShell
{
    //same as before
} 

 修改视图,并通过附加属性Bind.Model为视图设置对应的ViewModel。它将通过Key从Ioc容器中解析对应的ViewModel。 

<UserControl x:Class="Caliburn.Micro.ViewFirst.ShellView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:cal="http://www.caliburnproject.org"
             cal:Bind.Model="Shell">
    <StackPanel>
        <TextBox x:Name="Name" />
        <Button x:Name="SayHello"
                Content="Click Me" />
    </StackPanel>
</UserControl> 

常见的接口和基类

由于一些功能经常用到,Caliburn.Micro封装成了一些基类,如下所示:

  • PropertyChangedBase,实现了INotifyPropertyChangedEx (and thus INotifyPropertyChanged)接口,提供了一个基于Lambda表达式的NotifyOfPropertyChange 方法,来支持标准的字符串机制,允许强有力的类型变更通知,当然,所有的属性变更事件会自动发送到UI线程。
  • BindableCollection,通过继承标准的ObservableCollection来实现IObservableCollection接口,并且通过实现INotifyPropertyChangedEx接口,这个类确保所有的属性变更和列表变化能够发送的UI线程。
  • IScreen,这个接口由IHaveDisplayName,IActivate, IDeactivate, IGuardClose and INotifyPropertyChangedEx构成。
  • Screen,派生自 PropertyChangedBase并且实现了 IScreen, IChild ,IViewAware接口。

上述这些意味着在ViewModel中,可以继承自PropertyChangedBase 或者 Screen。通俗点讲,当需要激活功能的时候用Screen,其他的则可以使用PropertyChangedBase。 

同样当使用了生命周期,则需要角色来管理它,而Conductor,在Caliburn.Micro中,通过实现IConductor接口来实现,此接口提供了如下方法:

  • ActivateItem,通过调用此方法来激活一部分项。
  • DeactivateItem,通过调用次方来销毁一部分项。
  • ActivationProcessed,当conductor激活一个项目时被调用,标志是否激活成功。
  • GetChildren,通过此方法返回conductor正在追踪 的项的列表。
  • INotifyPropertyChangedEx,Conductor同样实现了此接口。
  • ActiveItem,表示当前正在被激活的项。 

作为一个Presentation的框架,各个UI部件(Widget或者叫Pad)的管理是必不可少的。Screen就是用来表示UI部件的,它定义了一些列UI部件的生命期管理函数,比如Activated,DeActivated,TryClose等,并且可以通过GetView可以获得对应的View对象。因为Screen实现了很多功能,所以个人建议所有ViewModel都继承自Screen。Conductor可以用来管理Screen,不同的Screen可以用一个Conductor来管理,Conductor也使用了策略模式允许更改对Screen的处理;继承Conductor<>的ViewModel可以调用ActiveItem等方法管理继承自Screen的ViewModel;Conductor本身也是个Screen,因为Conductor也继承了Screen类。相关继承关系如下:

  • INotifyPropertyChangedEx : INotifyPropertyChanged
  • IScreen : IHaveDisplayName, IActivate, IDeactivate, IGuardClose, INotifyPropertyChangedEx
  • PropertyChangedBase : INotifyPropertyChangedEx
  • ViewAware : PropertyChangedBase, IViewAware
  • Screen : ViewAware, IScreen, IChild 

从Screen派生和窗口管理有关的类的继承关系(可以查看了Window Manager、Screen和Conductor后再看):

  • IConductor : IParent, INotifyPropertyChangedEx
  • ConductorBase : Screen, IConductor, IParent
  • AllActive : ConductorBase
  • ConductorBaseWithActiveItem : ConductorBase, IConductActiveItem
  • Conductor : ConductorBaseWithActiveItem
  • OneActive : ConductorBaseWithActiveItem 

Caliburn.Micro中的约定

Caliburn.Micro的主要功能之一是它能够通过一系列的约定来消除对样板代码的需要,默认约定处于ON状态,如果不需要可以关闭。

ViewModel和View的约定,在 ViewModel-First 中,我们有一个现有的 ViewModel,需要将其渲染到屏幕上。为此,CM 使用简单的命名模式来查找它应绑定到 ViewModel 并显示的 UI视图。这种命名模式就是ViewModel去掉Model就是对应的视图名称,如:CustomerViewModel默认情况下,对应的视图为CustomerView。这是通过ViewLocator.LocateForModelType 实现的。

尽管 Caliburn.Micro 更喜欢 ViewModel-First 开发,但有时您可能希望采用 View-First 方法。在View-First模式下,我们采用ViewModelLocator.LocateForViewType来实现命名模式。这种命名模式就是将视图中的View替换成ViewModel,如果找到匹配项,则从Ioc容器中解析它。

ViewModelLocator.LocateForView首先检查View实例的DataContext,查看之前是否缓存或自定义创建了ViewModel,如果为空,只有这样才会调用LocateForViewType,去查找对应的ViewModel。 ViewModelBinder视图模型,当我们将您的 View 和 ViewModel 绑定在一起时,无论您使用的是 ViewModel-First 还是 View-First 方法,都会调用 ViewModelBinder.Bind 方法。此方法将 View 的 Action.Target 设置为 ViewModel,并相应地将 DataContext 设置为相同的值。它还会检查您的 ViewModel 是否实现 IViewAware,如果是,则将 View 传递给您的 ViewModel。ViewModelBinder 做的最后一件重要事情是确定它是否需要创建任何常规属性绑定或操作。为此,它会在界面中搜索绑定/操作的候选元素列表,并将其与 ViewModel 的属性和方法进行比较。找到匹配项后,它会代表您创建绑定或操作。

如果不喜欢 ViewModelBinder 的行为,,它遵循与上述框架服务相同的模式。它有几个 Func,您可以用自己的实现替换,例如 Bind、BindActions 和 BindProperties。如果想要关闭,请将 ViewModelBinder.ApplyConventionsByDefault 设置为 false。如果要逐个视图启用它,可以在 View 上将 View.ApplyConventions 附加属性设置为 true。 

如果你仔细检查,你会发现上面的两个约定之间存在细微的差异。只需将“ViewModel”添加到以“Page”为后缀的名称中,即可生成其 ViewModel 的名称。但是,只有 “Model” 会添加到以 “View” 为后缀的名称中,以生成其配套 ViewModel 的名称。这种差异主要源于将某项内容命名为“MainViewViewModel”而不是“MainPageViewModel”的语义尴尬。因此,从“View”后缀的 View 名称派生的 ViewModel 的命名约定通过将 ViewModel 命名为“MainViewModel”来避免冗余。 

命名空间约定,Caliburn.Micro建议ViewModel和View在同一个命名空间中。 也可以根据不同的功能分类,将视图和ViewModel组织在不同的文件夹中。 

事件聚合器

Caliburn.Micro默认提供了一个EventAggregator,它提供了一种以松散的方式将对象从一个实体发布到另一个实体的能力。Event Aggregator 实际上是一种设计模式,它的实现可能因框架而异。要正确的使用EventAggregator,它必须作为应用程序级别服务存在,通常是将它创建为单例来实现,建议通用Ioc容器进行注入和获取事件聚合器。

在Bootstrapper中的Configure方法中,注入事件聚合器对象。

发布事件,获取事件聚合器对象后,就可以调用,可以用事件聚合器发送任何对象。

public class FooViewModel {
        private readonly IEventAggregator _eventAggregator;

        public FooViewModel(IEventAggregator eventAggregator) {
            _eventAggregator = eventAggregator;

            _eventAggregator.PublishOnUIThread(new object());
            _eventAggregator.PublishOnUIThread("Hello World");
            _eventAggregator.PublishOnUIThread(22);
        }
 }

订阅,任何实体都可以通过 Subscribe 方法将自身提供给 EventAggregator 来订阅任何 Event。

public class FooViewModel : IHandle<object> {
    private readonly IEventAggregator _eventAggregator;

    public FooViewModel(IEventAggregator eventAggregator) {
        _eventAggregator = eventAggregator;
        _eventAggregator.Subscribe(this);
    }

    public void Handle(object message) {
        // Handle the message here.
    }
}

为了使此功能易于使用,我们提供了一个特殊接口 (IHandle),用于将订阅者标记为对给定类型的 Event 感兴趣。

请注意,通过实现上面的接口,如果 T 是您指定感兴趣的消息类型,您将被迫实现方法 Handle(T message)。此方法是在发布匹配的 Event 类型时调用的。 

单个实体想要侦听多个事件类型的情况并不少见。由于我们使用泛型,这就像向订阅者添加第二个 IHandle 接口一样简单。请注意,Handle 方法现在使用新的 Event 类型重载。

public class FooViewModel : IHandle<object> {
    private readonly IEventAggregator _eventAggregator;

    public FooViewModel(IEventAggregator eventAggregator) {
        _eventAggregator = eventAggregator;
        _eventAggregator.Subscribe(this);
    }

    public void Handle(object message){
        if (_eventAggregator.HandlerExistsFor(typeof(SpecialMessageEvent))){
            _eventAggregator.PublishOnUIThread(new SpecialEventMessage(message));
        }
    }
}

Caliburn.Micro 的 EventAggregator 支持多态性。选择要调用的处理程序时,EventAggregator 将触发任何 Event 类型的处理程序,该处理程序可从正在发送的 Event 中分配。这带来了很大的灵活性,并有助于重用。

public class FooViewModel : IHandle<string>, IHandle<bool> {
    private readonly IEventAggregator _eventAggregator;

    public FooViewModel(IEventAggregator eventAggregator) {
        _eventAggregator = eventAggregator;
        _eventAggregator.Subscribe(this);
    }

    public void Handle(string message) {
        // Handle the message here.
    }

    public void Handle(bool message) {
        // Handle the message here.
    }
}

在下面的示例中,由于 String 派生自 System.Object,因此在发布 String 消息时,将调用这两个处理程序。

public class FooViewModel : IHandle<object>, IHandle<string> {
    private readonly IEventAggregator _eventAggregator;

    public FooViewModel(IEventAggregator eventAggregator) {
        _eventAggregator = eventAggregator;
        _eventAggregator.Subscribe(this);
        _eventAggregator.PublishOnUIThread("Hello");
    }

    public void Handle(object message) {
        // This will be called
    }

    public void Handle(string message) {
        // This also
    }
}

查询事件处理程序,Caliburn.Micro提供了一种机制来查询 EventAggregator 以查看给定的 Event 类型是否具有任何处理程序,这在假定至少存在一个处理程序的特定情况下非常有用。 

Ioc容器

Caliburn.Micro 预先设置了一个名为 SimpleContainer 的依赖注入容器。依赖注入实际上是一种模式,通常使用容器元素而不是手动服务映射。

在向 SimpleContainer 添加任何服务绑定之前,请务必记住,容器本身必须向 Caliburn.Micro 注册,框架才能使用上述绑定。此过程会将 SimpleContainer 注入 IoC,这是 Caliburn.Micro 的内置 Service Locator。

  • RegisterInstance 方法允许针对类型和/或键向容器注册预构造的实例。
  • RegisterPerRequest 注册要针对类型和/或键注册的实现。
  • RegisterSingleton 针对类型、键或两者注册实现,而 Singleton 则重载以允许针对实现的类型或它实现或继承的另一种类型进行注册。
  • 构造函数注入是使用最广泛的依赖注入形式,它表示服务与它们被注入的类之间所需的依赖关系。当您需要对给定服务进行非可选使用时,应使用构造函数注入。
  • Property Injection 提供了将服务注入到依赖项容器外部创建的实体的功能。当实体被传递到 BuildUp 方法中时,将检查其属性,并使用与上述相同的递归逻辑注入任何可用的匹配服务。 

 在大多数情况下,构造函数注入是最好的选择,因为它使服务要求明确,但是属性注入有很多用例。需要注意的是,属性注入仅适用于 Interface 类型。 

public class ShellViewModel {
    private readonly IWindowManager _windowManager;

    public ShellViewModel(IWindowManager windowManager) {
        _windowManager = windowManager;
    }
}

通过容器获取单个服务。通过Ioc的Get方法可以获取服务,可以获取默认的服务,也可以通过指定的Key获取服务。

//获取单个服务
var windowManager = IoC.Get<IWindowManager>();
var windowManager = IoC.Get<IWindowManager>("windowManager");

获取服务列表,通过Ioc的GetAll方法,可以获取满足条件的服务列表,返回类型为 IEnumerable<T>,其中T是请求的服务类型。

//获取服务列表
var viewModelCollection = IoC.GetAll<IViewModel>();
var viewModel = IoC.GetAll<IViewModel>().FirstOrDefault(vm => vm.GetType() == typeof(ShellViewModel));

注入服务实例,通过Ioc的BuildUp方法,可以将实例对象注入到容器中。 

//注入实例
var viewModel = new LocallyCreatedViewModel();
IoC.BuildUp(viewModel);

MVVM简介

MVVM 代表 Model、View、View Model。基本思想是创建一个三层用户界面。它的主要用途是将 UI 的可视部分(表单、按钮和其他控件)尽可能与处理 UI 命令的逻辑分开。对于 WPF 应用程序,视图由 XAML 代码和代码隐藏表示,这实质上是用 C# 编写 XAML 代码的方法。此层不应执行任何处理。View Model 是功能性 UI 层,它接受来自 UI 的命令并处理这些命令。第三层是 Model,它保存数据模型。 例如,如果用户按下 View 中的按钮,则会向 View Model 发送一个命令,该命令将确定要做什么并执行该命令。在本教程的后面部分,您将看到更多内容。Model 应用于表示对象的 (自定义) 数据模型。 您可以将这三个层与程序逻辑 (功能层) 和数据访问层分开,这将将您的功能层与数据访问分开。 

如果您创建一个非常简单的应用程序,则可能不需要使用 MVVM。MVVM 将在您的应用程序上产生一些开销,您需要一些时间来学习如何应用 MVVM。如果您想使用自动化测试,您很快就会发现通过 UI 进行测试很困难,并且由于 UI 通常会频繁更改,因此很难维护测试。在这种情况下,View Model 是一个更好的测试访问点。您可以使用常规的单元测试方法。 

您可以很好地开始使用 MVVM 原则,而无需使用 Caliburn.Micro。从第一天开始,将 Caliburn.Micro 用于新项目的工作量较少,但如果您已经使用 View-ViewModel 分离,则在许多情况下可以非常快速地迁移到 Caliburn.Micro。即使您尚未使用 Caliburn.Micro,也可以开始使用 Caliburn.Micro。在大多数情况下,它不会影响现有代码。您也不需要采用所有 Caliburn.Micro 功能。它是灵活的。 

MVVM应用

在Caliburn.Micro框架中,开始MVVM,主要步骤如下:

  • 创建应用程序,安装Caliburn.Micro包,创建文件夹目录Models,ViewModels,Views分别存放相应文件。
  • 配置Caliburn.Micro框架,调整App.xaml,删除默认的StartupUri启动项,删除MainWindow.xaml。
  • 配置Caliburn.Micro的启动项Bootstrapper.cs,
  • 创建默认ShellViewModel作为程序主窗口,创建默认视图ShellView.xaml。
  • ShellViewModel是控制应用程序的逻辑的中央ViewModel。它会激活其他ViewModel,并包含一些通用逻辑。注意ShellViewModel继承自Conductor。
  • 在ShellView.xaml中,添加ContentControl布局容器,并命名为ActiveItem,用于显示用户控件。 

ShellView.xaml页面内容如下所示:

<Window x:Class="DemoCaliburn.Views.ShellView"
        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:local="clr-namespace:DemoCaliburn.Views"
        mc:Ignorable="d"
        Title="ShellView" Height="450" Width="800">
    <Grid>
        <ContentControl x:Name="ActiveItem" Margin="20"/>
    </Grid>
</Window>

配置Bootstrapper,注意事项如下所示:

  • 确保Bootstrapper继承自Bootstrapperbase。
  • 创建构造函数,并调用Initialize()方法。
  • 重写OnStartup事件,通过DisplayRootViewForAsync(typeof(ShellViewModel))配置默认启动视图。
  • 打开App.xaml,删除默认的启动项StartupUri="MainWindow.xaml"。 添加默认的启动Bootstrapper。
  • 添加日志,在Bootstrapper中添加,LogManager.GetLog = type => new DebugLog(type); 

MVVM实例

创建模型,首先创建一个Category类,包含Id,Name,Description三个属性,并保存在Models文件夹。

namespace DemoCaliburn.Models
{
    public class Category
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Description { get; set; }
    }
}

创建ViewModel,创建一个CategoryViewModel,存放在ViewModels文件夹,CategoryViewModel继承自Screen。 类型为BindableCollection<Category> CategoryList是所有类别列表,用于绑定UI视图的DataGrid 类型为Category SelectedCategory的表示列表选中项。 在属性CategoryName,CategoryDescription的set方法中增加了NotifyOfPropertyChange(nameof(CategoryName));语句来实现更改通知的功能。 重载了Screen的OnViewLoaded方法,此方法表示页面加载完成后调用。  增加了Edit(编辑)、Delete(删除)、Save (保存) 和 Clear(清除)。 

namespace DemoCaliburn.ViewModels
{
    public class CategoryViewModel:Screen
    {
        private BindableCollection<Category> _categoryList = new BindableCollection<Category>();
        public BindableCollection<Category> CategoryList
        {
            get
            {
                return _categoryList;
            }
            set
            {
                _categoryList = value;
            }
        }

        private Category _selectedCategory;
        public Category SelectedCategory
        {
            get
            {
                return _selectedCategory;
            }

            set
            {
                _selectedCategory = value;
                NotifyOfPropertyChange(nameof(SelectedCategory));
                NotifyOfPropertyChange(() => CanEdit);
                NotifyOfPropertyChange(() => CanDelete);
            }
        }

        private string _categoryName;
        public string CategoryName
        {
            get => _categoryName; set
            {
                _categoryName = value;
                NotifyOfPropertyChange(nameof(CategoryName));
            }
        }
        
        private string _categoryDescription;
        public string CategoryDescription
        {
            get => _categoryDescription; set
            {
                _categoryDescription = value;
                NotifyOfPropertyChange(nameof(CategoryDescription));
            }
        }

        protected override void OnViewLoaded(object view)
        {
            base.OnViewLoaded(view);
            CategoryList.Add(new Category { Id=1, Name = "Meals", Description = "Lunched and diners" });
            CategoryList.Add(new Category { Id=2, Name = "Representation", Description = "Gifts for our customers" });
        }

        public bool CanEdit
        {
            get
            {
                return SelectedCategory != null;
            }
        }

        public void Edit()
        {
            CategoryName = SelectedCategory.Name;
            CategoryDescription = SelectedCategory.Description;
        }

        public bool CanDelete
        {
            get
            {
                return SelectedCategory != null;
            }
        }

        public void Delete()
        {
            CategoryList.Remove(SelectedCategory);
            Clear();
        }

        public void Save()
        {
            Category newCategory = new Category();
            newCategory.Name = CategoryName;
            newCategory.Description = CategoryDescription;
            if (SelectedCategory != null)
            {
                CategoryList.Remove(SelectedCategory);
            }
            CategoryList.Add(newCategory);
            Clear();
        }

        public void Clear()
        {
            CategoryName = string.Empty;
            CategoryDescription = string.Empty;
            SelectedCategory = null;
        }
    }
}

在ShellViewModel中挂载CategoryViewModel,首先在创建一个方法,此方法主要用于从Ioc容器中获取CategoryViewModel的实例,并调用ActivateItemAsync激活此实例。

public Task EditCategories()
{
    var viewmodel = IoC.Get<CategoryViewModel>();
    return ActivateItemAsync(viewmodel, new CancellationToken());
}

重载ShellViewModel的OnViewLoaded方法,并在此方法中调用EditCategories方法。 

protected async override void OnViewLoaded(object view)
{
    base.OnViewLoaded(view);
    await EditCategories();
}

创建视图CategoryView.xaml,并保存在Views目录下,在此视图中DataGrid主要用到两个属性ItemsSource和SelectedItem。

<DataGrid ItemsSource="{Binding CategoryList, NotifyOnSourceUpdated=True}" 
    SelectedItem="{Binding SelectedCategory}"
    AutoGenerateColumns="False" Height="200" Margin="10">
    <DataGrid.Columns>
        <DataGridTextColumn Header="Id" Binding="{Binding Id}" Width="80"/>
        <DataGridTextColumn Header="Name" Binding="{Binding Name}" Width="80"/>
        <DataGridTextColumn Header="Description" Binding="{Binding Description}" Width="200"/>
    </DataGrid.Columns>
</DataGrid>

文本框TextBox的绑定采用命名约定,将文本的名称x:Name和CategoryViewModel中的属性保持一致,就可自动绑定。

<StackPanel Grid.Column="1">
    <TextBlock HorizontalAlignment="Center" FontSize="20" FontWeight="Bold" Margin="10">Edit Category</TextBlock>
    <WrapPanel Margin="5">
        <TextBlock Width="120">Category Name</TextBlock>
        <TextBox x:Name="CategoryName" Text="{Binding CategoryName, Mode=TwoWay}" Width="80"/>
    </WrapPanel>
    <WrapPanel Margin="5">
        <TextBlock Width="120">Category Description</TextBlock>
        <TextBox x:Name="CategoryDescription" Text="{Binding CategoryDescription, Mode=TwoWay}" Width="160"/>
    </WrapPanel>
    <WrapPanel>
        <Button x:Name="Save" Width="80" Margin="5">Save</Button>
        <Button x:Name="Clear" Width="80" Margin="5">Clear</Button>
    </WrapPanel>
</StackPanel>

Button按钮,也可以不写OnClick事件,通过命名约定,将CategoryViewModel中的方法和Button自动绑定。 

<WrapPanel>
        <Button x:Name="Edit" Width="80" Margin="5">Edit</Button>
        <Button x:Name="Delete" Width="80" Margin="5">Delete</Button>
</WrapPanel>

条件执行,如果属性的名称以Can开头,后跟方法名称,如方法Edit对应的属性CanEdit,遵循此命名约定,则可以根据属性的返回值控制按钮是否允许执行。如:当列表没有选中项时,Edit按钮不可用 

public bool CanEdit
{
	get
	{
		return SelectedCategory != null;
	}
}

public void Edit()
{
	CategoryName = SelectedCategory.Name;
	CategoryDescription = SelectedCategory.Description;
}

菜单,前面讲述了MVVM的基础功能,接下来继续讲解菜单,主要是在ShellView页面添加菜单。

<Menu>
	<MenuItem x:Name="FileMenu" Header="File" IsEnabled="{Binding CanFileMenu}"/>
	<MenuItem Header="Edit"/>
	<MenuItem Header="Settings">
		<MenuItem x:Name="EditCategories" Header="Edit Categories"/>
	</MenuItem>
	<MenuItem Header="Help">
		<MenuItem Header="Manual"/>
		<MenuItem x:Name="About" Header="About"/>
	</MenuItem>
</Menu>

MenuItems看起来好像不支持直接命名约定,为了启用或者禁用一个MenuItem,需要增加依赖属性绑定。如:在ViewModel中,增加一个CanFileMenu属性,来控制FileMenu的使用。当然需要在其他属性变化时,通过PropertyChanged事件来通知它的变化来实现动态控制。 

public bool CanFileMenu
{
	get
	{
		return false;
	}
}

弹出窗口,首先创建AboutView.xaml,并保存在Views文件夹下,用于弹出关于窗口页面。

<Window x:Class="DemoCaliburn.Views.AboutView"
        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:local="clr-namespace:DemoCaliburn.Views"
        mc:Ignorable="d"
        WindowStartupLocation="CenterScreen" SizeToContent="WidthAndHeight"
        Title="About" Height="450" Width="800">
    <StackPanel>
        <Grid Margin="20">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="Auto" MinWidth="150"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="*"/>
            </Grid.RowDefinitions>
            <TextBlock Grid.Row="0" Grid.Column="0" Margin="5">Program name:</TextBlock>
            <TextBlock x:Name="AboutData_Title" Grid.Row="0" Grid.Column="1" Margin="5"/>
            <TextBlock Grid.Row="1" Grid.Column="0" Margin="5">Program version:</TextBlock>
            <TextBlock x:Name="AboutData_Version" Grid.Row="1" Grid.Column="1" Margin="5"/>
            <TextBlock Grid.Row="2" Grid.Column="0" Margin="5">Author:</TextBlock>
            <TextBlock Grid.Row="2" Grid.Column="1" Margin="5" Text="{Binding AboutData.Author}"/>
            <TextBlock Grid.Row="3" Grid.Column="0" Margin="5">More information:</TextBlock>
            <TextBlock Grid.Row="3" Grid.Column="1" Margin="5">
         <Hyperlink RequestNavigate="Hyperlink_RequestNavigate"
               NavigateUri="{Binding AboutData.Url}">
            <TextBlock Text="{Binding AboutData.Url}"/>
         </Hyperlink>
      </TextBlock>
        </Grid>
        <Button x:Name="CloseForm" Margin="5" HorizontalAlignment="Right" Width="80">Close</Button>
    </StackPanel>
</Window>

其中点击超链接事件在后台隐藏代码中。 

public partial class AboutView : Window
{
	public AboutView()
	{
		InitializeComponent();
	}

	private void Hyperlink_RequestNavigate(object sender, System.Windows.Navigation.RequestNavigateEventArgs e)
	{
		var psi = new ProcessStartInfo
		{
			FileName = e.Uri.AbsoluteUri,
			UseShellExecute = true
		};
		Process.Start(psi);
	}
}

AboutView视图对应的模型为About。创建模型,并保存在Models目录。

public class About
{
	public string Title { get; set; } = "Caliburn.Micro 示例";
	public string Version { get; set; } = "1.0";
	public string Author { get; set; } = "老码识途";
	public string Url { get; set; } = "老码识途";
}

创建AboutViewModel并保存在ViewModels目录下,其中属性AboutData主要用于绑定页面属性。

public class AboutViewModel : Screen
{
	public AboutViewModel()
	{
	}

	private About _aboutData = new About();

	public About AboutData
	{
		get { return _aboutData; }
	}

	public Task CloseForm()
	{
		//TryClose(true); // for OK
		//TryClose(false); // for Cancel
		return TryCloseAsync();
	}
}

ViewModel中属性和View页面采用命名约定,x:Name=”属性名”,主要属性类型为自定义的类,则子属性名与视图绑定也遵循命名约定,两个属性名用下划线连接,如x:Name=”属性名_子属性名” 

弹出窗口需要用到IWindowManager接口,在ViewModel的构造函数进行注入。

private readonly IWindowManager _windowManager;

public ShellViewModel(IWindowManager windowManager)
{
	_windowManager = windowManager;
}

创建菜单About命名约定的函数About方法。在方法中通过windowManager的ShowDialogAsync方法弹出窗口。且ShowDialogAsync方法返回一个bool?类型的值,表示是否ok. 

public Task About()
{
    return _windowManager.ShowDialogAsync(IoC.Get<AboutViewModel>());
}

除了弹出模态窗口,还可以弹出窗口的其他变体,如:非模态窗口,鼠标位置弹出Popup窗口等。 在弹出窗口时,还可以设置窗口的一些参数,此参数为IDictionary类型的字典。 

public Task About()
{
	//dynamic settings = new ExpandoObject();
	//settings.WindowStartupLocation = WindowStartupLocation.CenterOwner;
	//settings.ResizeMode = ResizeMode.NoResize;
	//settings.MinWidth = 450;
	//settings.Title = "My New Window";
	//settings.Icon = new BitmapImage(new Uri("pack://application:,,,/MyApplication;component/Assets/myicon.ico"));

	//IWindowManager manager = new WindowManager();
	//manager.ShowDialog(myViewModel, null, settings);
	//_windowManager.ShowPopupAsync(IoC.Get<AboutViewModel>()); //当前鼠标位置显示一个弹出表单
	//_windowManager.ShowWindowAsync(IoC.Get<AboutViewModel>());//普通窗口
	return _windowManager.ShowDialogAsync(IoC.Get<AboutViewModel>());//模态窗口
}

 

源码下载

Calibun.Micro是一款开源框架,在Github上可以进行下:

Github网址:https://github.com/Caliburn-Micro/Caliburn.Micro 

 

以上就是《WPF开发框架Caliburn.Micro详解》的全部内容,旨在抛砖引玉,一起学习,共同进步!!!

标签:eventAggregator,ViewModel,public,Micro,WPF,Caliburn,View
From: https://www.cnblogs.com/hsiang/p/18606215

相关文章