Prism框架

使用

NuGet包下载 -- > Prism.Unity

框架中的数据与行为

BindableBase

在ViewModel中需要继承此类

通知属性的三种方式:

public class MainViewModel : BindableBase
{
    // 基本的通知属性
    private string _value;

    public string Value
    {
        get { return _value; }
        set
        {
            // 通知属性 第一种方式
            SetProperty<string>(ref _value, value);
            // 第二种方式
            //_value = value;
            //this.RaisePropertyChanged("Value");
            // 第三种方式
            //_value = value;
            //this.OnPropertyChanged(new System.ComponentModel.PropertyChangedEventArgs("Value"));
        }
    }
}

DelegateCommand/DelegateCommand<T>

 // 命令属性定义
 public DelegateCommand<object> BtnCommand { get; set; }

 public MainViewModel()
 {
     // 初始化
     BtnCommand = new DelegateCommand<object>((arg) =>
     {

     });
 }

基本逻辑处理

IOC:项目初始化,Unity

安装Prism.Unity -- NuGet包

在APP.xaml中需要改为:需要StartupUri 去掉

<p:PrismApplication x:Class="XH.PrismInitialize.App"
                    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:local="clr-namespace:XH.PrismInitialize"
                    xmlns:p="http://prismlibrary.com/">
  <Application.Resources>

  </Application.Resources>
</p:PrismApplication>

并且APP.xaml.cs文件中,APP也要继承PrismApplication类,并且完成以下两个抽象方法:

在CreateShell()方法中,写初始化哪个窗口

public partial class App : PrismApplication
 {
    public App()
    {
        
    }
    // 初始化第一种方式
    // 提供主窗口的对象
    protected override Window CreateShell()
    {
        return new MainWindow() { Title = "Prism Start"};
    }

    // 业务中所需要的注入对象,在这个方法里注册
    protected override void RegisterTypes(IContainerRegistry containerRegistry)
    {

    }
}

初始化也可以用第二种方法:PrismBootstrapper

需要写个信的类,继承PrismBootstrapper

这里面重写的两个方法,和刚才的两个抽象方法是一样的作用

 // 启动项
 public class StartUp : PrismBootstrapper
 {

     // 提供主窗口的对象
     protected override DependencyObject CreateShell()
     {
         return new MainWindow() { Title = "Prism Start" };
     }

     // 业务中所需要的注入对象,在这个方法里注册
     protected override void RegisterTypes(IContainerRegistry containerRegistry)
     {
         
     }
 }

在APP.xaml.cs的构造函数中写入: 即可

 public App()
 {
     new StartUp().Run();
 }

注入IEventAggregator

注入使用

第一步:在需要使用的ViewModel中构造函数注入即可:

public MainWindow(IEventAggregator eventAggregator)
{
    InitializeComponent();
}

第二步:需要在APP的CreateShell方法中进行写注入:

以下两种写法都可以

protected override Window CreateShell()
{
    // 第一种
    //var ea = Container.Resolve<IEventAggregator>();
    //return new MainWindow(ea) { Title = "Prism Start" };
    
    // 第二种
    return Container.Resolve<MainWindow>();
}

静态使用

单独创建一个类,进行初始化IEventAggregator类,进行静态调用即可:

public class Messenger
{
    private Messenger() { }
    public static IEventAggregator Defualt { get; set; }
}

在APP中进行初始化

Messenger.Defualt = Container.Resolve<IEventAggregator>();

属性注入、参数多实现注入

以下代码中,静态和注入都是一样的,都是同一个消息总线对象

GetEvents<T> where T : EventBase, new();

订阅无参
public MainWindow(IEventAggregator eventAggregator)
{
    InitializeComponent();
    // 订阅无参
    eventAggregator.GetEvent<EventMessage>().Subscribe(Receive);
    // 发布
    eventAggregator.GetEvent<EventMessage>().Publish();
}

// 无参数触发 
private void Receive()
{

}
订阅有参:
public MainWindow(IEventAggregator eventAggregator)
{
    InitializeComponent();

    // 订阅带参
    // 全局静态处理 和 eventAggregator 是同一个对象
    Messenger.Defualt.GetEvent<EventMessageArgs>().Subscribe(Receive);
    // 静态发布
    Messenger.Defualt.GetEvent<EventMessageArgs>().Publish("Hello");
}

// 有参数触发
private void Receive(object obj)
{

}
订阅List:
public MainWindow(IEventAggregator eventAggregator)
{
    InitializeComponent();
    // 类订阅
    Messenger.Defualt.GetEvent<EventMessageArgsList<EventMessageList>>().Subscribe(Receive);

    // 类触发
    Messenger.Defualt.GetEvent<EventMessageArgsList<EventMessageList>>().Publish(new EventMessageList()
    {
        Name = "张三",
        Age = 18
    });
}
// list 触发
private void Receive(EventMessageList list)
{

}
利用委托传值:

事件类:

// 无参数触发
public class EventMessage : PubSubEvent { }

// 参数触发
public class EventMessageArgs : PubSubEvent<object> { }

// 类型参数触发
public class EventMessageArgsList<T> : PubSubEvent<T> { }

public class EventAction
{
    public Action<bool> ResultAction { get; set; }
}

在SubWin中订阅:

public SubWindow()
{
    InitializeComponent();

    // 订阅
    Messenger.Defualt.GetEvent<EventMessageArgs>().Subscribe(Receive);
}

private void Receive(object obj)
{
    var ea = (EventAction)obj;
    ea.ResultAction?.Invoke(true);
}

在MainWin中发布:

// 发布
Messenger.Defualt.GetEvent<EventMessageArgs>().Publish(new EventAction()
{
    ResultAction = new Action<bool>(state =>
    {

    })
});

触发顺序:SubWin订阅--> MainWin发布-->SubWin 的 Receive方法 --> MainWin 的 ResultAction方法

订阅参数
无参数订阅
Messenger.Defualt.GetEvent<EventMessageArgs>().Subscribe(Receive);
 private void Receive() { }
过滤参数订阅

第一个参数:执行方法

第二个参数:过滤器,可以根据什么条件进行过滤,满足条件之后才会执行第一个参数方法

订阅:

Messenger.Defualt.GetEvent<EventMessageArgs>().Subscribe(Receive, obj => obj.Id == 1);

private void Receive(DataModel obj) { }

发布:

Messenger.Defualt.GetEvent<EventMessageArgs>().
    Publish(new DataModel { Id = 2, Text = "Hello" });

以上的两个方法中,不会触发Receive,因为只过滤了 id == 1 的数据

消息委托的引用方式

第二个参数默认设置为false,指定委托的强引用和弱引用:

true:强引用,不关闭一直打开,在对象销毁的时候做注销操作;

false:弱引用,自动释放

Messenger.Defualt.GetEvent<EventMessageArgs>().Subscribe(Receive,true);
private void Receive(DataModel obj) { }
多线程控制

PublisherThread

发布者在什么线程发布的,注册的逻辑就在哪个线程执行 默认此方法

UIThread

不管发布者在什么线程发布的,注册的逻辑总是在非UI线程(主线程)执行

如果在执行逻辑里有页面的操作,可以使用这个

BackgroundThread

不管发布者在哪个线程发布,注册的逻辑总是在后台线程执行

例如写日志,不在UI线程和当前线程执行,就在后台线程执行

新建一个线程,把当前逻辑包起来 跟发布方无关

Messenger.Defualt.GetEvent<EventMessageArgs>().
    Subscribe(Receive1, Prism.Events.ThreadOption.BackgroundThread);

// 在这个里面可以获取线程ID
private void Receive1(DataModel obj)
{
    var id = Thread.CurrentThread.ManagedThreadId;
}

弹窗对象

使用

创建一个弹出窗口的内容:一般是UserControl(不是Window)

传递一个名称:窗口内容对象的名称

窗口内容:UCDetail UserControl

名称:UCDetail,应该是内容注册的时候给定的名称

前提是IOC容器中,创建一个UCDetail类型的对象

每次打开的页面都是新实例

window 会报错:

注入:IDialogService:用来执行弹窗动作,一般注入到某个VM中调用 一般是调用方

 public MainViewModel( IDialogService dialogService)
 {
     _dialogService = dialogService;
     OpenCommand = new DelegateCommand(OnOpen);
 }

注册弹窗内容(内容需要注册到IoC容器),对应ViewModel实现IDialogAware接口

在APP.xaml.cs中注册:

public partial class App : PrismApplication
{
    protected override Window CreateShell()
    {
        return Container.Resolve<MainWindow>();
    }

    protected override void RegisterTypes(IContainerRegistry containerRegistry)
    {
        // 注册弹窗内容
        containerRegistry.RegisterDialog<UCDetail>("Detail");

        // 注册弹窗窗口,这句代码会将框架内的默认弹窗窗口替换掉
        containerRegistry.RegisterDialogWindow<DialogParent>();
    } 
}

弹窗的xaml.cs文件:

 public UCDetail(IContainerProvider containerProvider)
 {
     InitializeComponent();

     // 1.明确需要获取某个对象 并且这个对象里需要自动注入一些内容
     // 2.在IOC创建的过程,需要注入的对象,都需要注册
     this.DataContext = containerProvider.Resolve<DetailViewModel>();
 }

弹窗的ViewModel:

public class DetailViewModel : IDialogAware
{
    // 弹出窗口的标题
    public string Title => "Hello Dialog";

    // 执行关闭返回的结果
    public event Action<IDialogResult> RequestClose;

    // 当前打开的窗口是否允许关闭
    public bool CanCloseDialog()
    {
        return true;
    }

    // 弹出窗口关闭时执行逻辑
    public void OnDialogClosed()
    {

    }

    // 弹出窗口打开时执行逻辑
    public void OnDialogOpened(IDialogParameters parameters)
    {
        
    }
}

窗口配置:在内容UserControl对象中,进行样式设置 p:Dialog.WindowStyle

可以根据设置window一样设置窗口

<p:Dialog.WindowStyle>
  <Style TargetType="Window">
    <Setter Property="Height" Value="300" />
    <Setter Property="Width" Value="300" />
    <Setter Property="WindowChrome.WindowChrome">
      <Setter.Value>
        <WindowChrome GlassFrameThickness="-1" />
      </Setter.Value>
    </Setter>
  </Style>
</p:Dialog.WindowStyle>

注册弹窗窗口,对应窗口实现IDialogWindow接口

1、新建一个window窗口,然后注册

// 注册弹窗窗口,这句代码会将框架内的默认弹窗窗口替换掉
containerRegistry.RegisterDialogWindow<DialogParent>();

窗口关闭状态

同过IDialogResult传值,然后穿给打开窗口方

窗口ViewModel:

public DelegateCommand CloseCommand { get; set; }

public DetailViewModel()
{
    CloseCommand = new DelegateCommand(OnClose);
}

private void OnClose()
{
    IDialogResult dialogResult = new DialogResult();
    dialogResult.Parameters.Add("A", true);
    dialogResult.Parameters.Add("value", Value);
    RequestClose?.Invoke(dialogResult);
}

调用方ViewModel:

 private void OnOpen()
 {
     // 获取弹窗关闭时的返回结果
     // ShowDialog(string name, Action<IDialogResult> callback)
     _dialogService.ShowDialog("Detail", OnDialogClosed);

 }

 private void OnDialogClosed(IDialogResult result)
 {
     // 根据键值对获取返回的值 做对应的处理
     Value = result.Parameters.GetValue<string>("value");
 }

总结

创建一个弹出窗口的内容:一般是UserControl(不是Window)

注入:IDialogService 用来执行弹窗动作,一般注入到某个VM中调用

注册弹窗内容(内容需要注册到IoC容器),对应ViewModel实现 IDialogAware接口

窗口配置:在内容UserControl对象中,进行样式设置 p:Dialog.WindowStyle

注册弹窗窗口,对应窗口实现 IDialogWindow接口

ViewModel的自动匹配

xaml中使用的属性:

p:ViewModelLocator.AutoWireViewModel="True":是否自动匹配

默认为True符合Prism的匹配规则

<Window x:Class="XH.PrismViewModelLocator.Views.LoginView"
  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:XH.PrismViewModelLocator.Views"
  xmlns:p="http://prismlibrary.com/"
  p:ViewModelLocator.AutoWireViewModel="True"
  mc:Ignorable="d"
  Title="LoginView" Height="450" Width="800">
  <StackPanel>
    <TextBlock Text="{Binding UserName}"  />
  </StackPanel>
</Window>

标准状态:

ViewModel与视图类型位于同一个程序集中

ViewModel位于.ViewModels(ViewModel)子命名空间中

View位于.Views(View)子命名空间中

ViewModel名称与视图名称对应,以“ViewModel”结尾

个性化配置:

在启动位置(PrismApplication/PrismBootstrapper)重写ConfigureViewModelLocator方法

ViewModelLocationProvider:

更改命名约定

默认的匹配规则:

protected override void ConfigureViewModelLocator()
{
    base.ConfigureViewModelLocator();

    // 1、配置默认的匹配规则
    ViewModelLocationProvider.SetDefaultViewTypeToViewModelTypeResolver(TypeResolver);
}

private Type TypeResolver(Type viewType)
{
    //XH.PrismViewModelLocator.Views.MainWindow
    //XH.PrismViewModelLocator.ViewModels.MainWindowViewModel
    //XH.PrismViewModelLocator.Views.LoginView
    //XH.PrismViewModelLocator.ViewModels.LoginViewModel

    var vmName = viewType.FullName.Replace(".Views", ".ViewModels");

    if (vmName.EndsWith("View"))
        vmName += "Model";
    else
        vmName += "ViewModel";

    return Type.GetType(vmName);
}

可以在TypeResolver方法中写自己想要的自定义匹配规则

独立注册-4种方式

强制将View和ViewModel 关联起来

 protected override void ConfigureViewModelLocator()
 {
     base.ConfigureViewModelLocator();

     //ViewModelLocationProvider.Register<MainWindow, MainWindowViewModel>();
     //
     //ViewModelLocationProvider.Register("MainWindow", typeof(MainWindowViewModel));
     //
     //ViewModelLocationProvider.Register("MainWindow", CreateMainViewModel);
     //
     //ViewModelLocationProvider.Register<MainWindow>(CreateMainViewModel);
 }
private object CreateMainViewModel() => Container.Resolve<MainWindowViewModel>();

Prism框架特色功能

区域化

区域注册

项目初始化

页面初始化:窗口(Shell)里进行区域划分,区域需要注册到RegionManager里的,才能进行内容接收

属性:

p:RegionManager.RegionName:注册的区域的名字

在Prism中四种容器类型可以注册区域导航:

  1. ContentControlRegionAdapter

ContentControl只能显示一个页面,单页面呈现

<ContentControl p:RegionManager.RegionName="ViewRegion"/>
  1. ItemsControlRegionAdapter
<ItemsControl p:RegionManager.RegionName="ViewRegion"/>
  1. SelectorRegionAdapter
<ListBox p:RegionManager.RegionName="ViewRegion"/>
<ListView p:RegionManager.RegionName="ViewRegion"/>
  1. TabControlRegionAdapter
<TabControl p:RegionManager.RegionName="ViewRegion"/>

注册的地方代码:

protected override DependencyObject CreateShell()
{
    return Container.Resolve<MainWindow>();
}

protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
    // 注册需要导航的子页面,只有注册了才能处理
    containerRegistry.RegisterForNavigation<ViewA>();
    containerRegistry.RegisterForNavigation<ViewB>();
}

MainWindow划分的代码:

<Window x:Class="XH.PrismRegion.Base.Views.MainWindow"
  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:XH.PrismRegion.Base.Views"
  xmlns:p="http://prismlibrary.com/"
  mc:Ignorable="d" FontSize="20"
  Title="MainWindow" Height="450" Width="800">
  <DockPanel>
    <Grid Height="50" DockPanel.Dock="Top" Background="Red">
      <TextBlock Text="Prism 框架" VerticalAlignment="Center" HorizontalAlignment="Left" Margin="20 0 0 0"/>
    </Grid>
    <Grid Width="220" DockPanel.Dock="Left" Background="LemonChiffon">
      <StackPanel>
        <Button Content="ViewA" Margin="0 3"/>
        <Button Content="ViewB" Margin="0 3"/>
        <Button Content="ViewB" Margin="0 3"/>
      </StackPanel>
    </Grid>
    <Grid Background="White">
      <TabControl p:RegionManager.RegionName="ViewRegion"/>
    </Grid>
  </DockPanel>
</Window>

MainWindow.xaml.cs注册区域代码:

public MainWindow(IRegionManager regionManager)
{
    InitializeComponent();

    regionManager.RegisterViewWithRegion("ViewRegion", "ViewA");
    regionManager.RegisterViewWithRegion("ViewRegion", "ViewB");


    this.Loaded += (se, ev) =>
    {
        // 激活这个注册的哪个View界面
        var region = regionManager.Regions["ViewRegion"];
        var view = region.Views.FirstOrDefault(v => v.GetType().Name == "ViewB");
        region.Activate(view);
    };
}
自定义Region

需要继承:RegionAdapterBase

public class CustomRegion : RegionAdapterBase<UniformGrid>
{
    public CustomRegion(IRegionBehaviorFactory regionBehaviorFactory)
        : base(regionBehaviorFactory)
        {

        }

    // 每次增加或者减少触发此方法
    protected override void Adapt(IRegion region, UniformGrid regionTarget)
    {
        region.Views.CollectionChanged += (o, e) =>
        {
            if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
            {
                foreach (UIElement view in region.Views)
                {
                    regionTarget.Children.Add(view);
                }
            }
            else if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Remove)
            {
                foreach (UIElement view in region.Views)
                {
                    regionTarget.Children.Remove(view);
                }
            }
        };
    }

    protected override IRegion CreateRegion()
    {
        // 返回激活的Region区域
        return new AllActiveRegion();
    }
}
注入:IRegionManager:管理项目中所有的Region区域的,处理区域中内容显示逻辑

注册导航页面:regionManager.RegisterViewWithRegion("ViewRegion", "ViewA");

执行导航动作,传参

观察导航过程:接收参数,页面对应的ViewModel实现INavigationAware接口

观察导航页面生命周期:控制页面是否自动销毁,页面对应ViewModel实现IRegionMemberLifetime接口

确认是否允许从当前页面导航出去:页面对应ViewModel实现IConfirmNavigationRequest接口

传参:NavigationParameters 同过Key Value 方式传参

调用方代码:

if (viewName == "ViewA")
{
    NavigationParameters parmaters = new NavigationParameters();
    parmaters.Add("A", "Hello");
    _regionManager.RequestNavigate("ViewRegion", viewName, parmaters);
}
else if (viewName == "ViewB")
{
    _regionManager.RequestNavigate("ViewRegion", viewName);
}

被打开方代码:

 // 打开当前View的时候触发
 public void OnNavigatedTo(NavigationContext navigationContext)
 {
     string arg = navigationContext.Parameters.GetValue<string>("A");
 }

注意:被打开方需要继承接口:INavigationAware


生命周期代码:需要继承IRegionMemberLifetime接口

用来控制当前页面非激活状态,是否在Region中保留

KeepAlive:

true:不会销毁

false:会销毁

public bool KeepAlive => true;

导航确认:是否允许能从当前页面导航去其他页面:需要继承IConfirmNavigationRequest

OnNavigatedFrom 调用前执行

public void ConfirmNavigationRequest(NavigationContext navigationContext, Action<bool> continuationCallback)
{
    // 从当前页面导航出去的时候 判断是不是需要导航出去
    // 打开某个页面
    // 
    if (MessageBox.Show("是否打开", "导航提示", MessageBoxButton.YesNo) == MessageBoxResult.Yes)
    {
        // 继续打开
        continuationCallback?.Invoke(true);
    }
    else
        // 不被打开
        continuationCallback?.Invoke(false);
}

整体ViewModel的执行顺序:

OnNavigatedTo --> ConfirmNavigationRequest --> OnNavigatedFrom


两个导航区域导航需要之间传参:

 // 从当前View导航出去的时候触发
 public void OnNavigatedFrom(NavigationContext navigationContext)
 {
     // 从当前页面到另外一个页面的时候 可以把这个信息带过去
     navigationContext.Parameters.Add("B", "Hello");
 }

也就是会所:A页面的OnNavigatedFrom触发之后,进入B界面之后,B界面的OnNavigatedTo会把A界面的参数带过来。

页面表头

主页面的XAML代码:

<TabControl p:RegionManager.RegionName="ViewRegion" Name="tb">
  <TabControl.ItemContainerStyle>
    <Style TargetType="TabItem">
      <!--TabItem这里的绑定数据源是页面对象
      如果需要页面对象的ViewModel 需要写:DataContext
      TabItem.DataContext = View 对象
      View 对象DataContext = 对应的ViewModel-->
      <Setter Property="Header" Value="{Binding DataContext.Title}"/>
    </Style>
  </TabControl.ItemContainerStyle>
</TabControl>

单独页面的ViewModel:

public string Title { get; set; } = "View A";
关闭TabItem(关闭注册的页面)
 private void DoCloseTab()
 {
     var region = _regionManager.Regions["ViewRegion"];
     // 需要关闭的页面
     var view = region.Views.FirstOrDefault(v => v.GetType().Name == "ViewA");
     region.Remove(view);

     // 关闭所有
     //region.RemoveAll();
 }
导航历史记录:需要注入接口:IRegionNavigationJournal

可以前一个后一个进行跳转

public ViewAViewModel(
    IRegionNavigationService regionNavigationService,
    IRegionNavigationJournal regionNavigationJournal,
    IRegionManager regionManager)
{
    _regionManager = regionManager;
    // 导航历史记录 操作
    regionNavigationJournal.GoBack();

    CloseTabCommand = new DelegateCommand(DoCloseTab);
}
导航日志的使用

IRegionNavigationJournal(接收对象)、IJournalAware(实现)

在页面中使用:

public class ViewAViewModel : INavigationAware
{
    // 接受导航日志
    public IRegionNavigationJournal Journal { get; set; }
    public ICommand GoBackCommand { get; set; }
    public ICommand ForwordCommand { get; set; }

    public ViewAViewModel(IRegionManager regionManager)
    {
        GoBackCommand = new DelegateCommand(() =>
        {
            // 跳转
            if (Journal.CanGoBack)
                Journal.GoBack();
        });

        ForwordCommand = new DelegateCommand(() =>
        {
            // 跳转
            if (Journal.CanGoForward)
                Journal.GoForward();
            else
            {
                // 下一步打开ViewB
                regionManager.RequestNavigate("MainRegion", "ViewB");
            }
        });
    }

    public void OnNavigatedTo(NavigationContext navigationContext)
    {
        // 跳转进来的时候 获取日志 
        Journal = navigationContext.NavigationService.Journal;
    }

    public bool IsNavigationTarget(NavigationContext navigationContext)
    {
        return true;
    }

    public void OnNavigatedFrom(NavigationContext navigationContext)
    {

    }
}
弹窗页面区域的注册 View中的区域注册(不是Shell)

背景:因为Prism中的注册Region 是在初始化CreateShell的时候,打开窗体时候,自动扫描此窗体中的所有的Region 注册的,弹窗是在初始化之后触发出来的,所以,需要在弹窗的地方重新注册Region 并且每次退出弹窗的时候,移除此窗体的Region

核心代码如下:

窗口的xaml.cs代码:

 public DialogView(IRegionManager regionManager)
 {
     InitializeComponent();
     // region 有个扫描时机,在Shell中会扫描一次,其他的需要手动加入并重新更新
     // 把当前的所有 region 都放入到regionManager中,重新扫描
     RegionManager.SetRegionManager(this, regionManager);
     // 更新下regionManager集合
     RegionManager.UpdateRegions();

     this.Unloaded += (o, e) =>
     {
         var rm = RegionManager.GetRegionManager(this);
         //rm.Regions.Remove("DialogRegion");
         // 释放所有的Name 等同于上面
         rm.Regions.FirstOrDefault(x => rm.Regions.Remove(x.Name));
     };
 }
复合命令 -- 保存全部 -- CompositeCommand

在主窗口赋值,然后同过注入CompositeCommand,在每个子页面中加入到CompositeCommand中,然后在主窗口可以命令所有加入的方法

主窗口ViewModel代码:

 // 多个命令整合一起
 public CompositeCommand AllSaveCommand { get; set; }

 public MainViewModel(IRegionManager regionManager, CompositeCommand compositeCommand)
 {
     AllSaveCommand = compositeCommand;
 }

子页面ViewModel代码:

 public ViewAViewModel(CompositeCommand compositeCommand)
 {
     SaveCommand = new DelegateCommand(() =>
     {

     });

     // 这里的前后关系很重要
     compositeCommand.RegisterCommand(SaveCommand);
 }

注意:必须先实例化Command 再注册Command

每次当前ViewModel 销毁的时候,或者子页面关闭的时候,退出注册。UnregisterCommand

 public void OnNavigatedFrom(NavigationContext navigationContext)
 {
     _compositeCommand.UnregisterCommand(SaveCommand);
 }

模块化项目基本结构

模块化例子:

项目的分模块管理

模块定义,创建WPF类库,添加类对象,实现IModule接口

模块注册

项目初始化类中添加(配置文件、类型添加、自动扫描)

步骤:
  1. 创建WPF项目:

需要在住项目中,重写ConfigureModuleCatalog方法:

// 模块注册
protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
{
    moduleCatalog.AddModule<AModule>();
}
  1. 其他模块创建WPF类库

在A模块中写一个AModule :需要继承IModule接口

public class AModule : IModule
{
    // 初始化
    public void OnInitialized(IContainerProvider containerProvider)
    {
        
    }

    // 注入
    public void RegisterTypes(IContainerRegistry containerRegistry)
    {
        containerRegistry.RegisterForNavigation<ViewA>();
    }
}

这个时候,两个项目就关联起来了,

这个时候两个项目的注入是同一个注入,Prism会自动合并为一个。

  1. 在主窗口使用
 public DelegateCommand OpenCommand { get; set; }
 public MainWindowViewModel(IRegionManager regionManager)
 {
     OpenCommand = new DelegateCommand(() =>
     {
         regionManager.RequestNavigate("MainRegion","ViewA");
     });
 }

效果:

模块注册
ConfigureModuleCatalog
  1. 通过 ConfigureModuleCatalog ,添加模块注册
 protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
 {
     moduleCatalog.AddModule<AModule>();
 }
  1. 通过 ConfigureModuleCatalog ,添加 ModuleInfo 进行注册
protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
{
     Type type = typeof(AModule);
     moduleCatalog.AddModule(new ModuleInfo
     {
         ModuleName = type.Name,
         ModuleType = type.AssemblyQualifiedName,
         // 标记Module按需加载 懒加载
         InitializationMode = InitializationMode.OnDemand,
     });
}
完全解耦

如果要实现完全解耦,两个模块没有任何关系,也不引用,那么需要使用生成命令操作:

需要module类库执行以下操作:

属性-->生成-->事件-->生成后事件

xcopy $(TargetPath) $(SolutionDir)20240727_WPFMvvm\XH.PrismLesson\XH.PrismModule\bin\Debug\net7.0-windows\ModulePath /y

解释:

  1. xcopy:是Windows命令行中的一个命令,用于复制文件和目录树。
  2. $(TargetPath):是一个宏,代表当前项目生成的输出文件的完整路径(包括文件名)。这通常是项目的主输出文件,比如一个DLL或EXE文件。
  3. $(SolutionDir):是另一个宏,代表解决方案目录的完整路径。但是,注意你的命令中$(SolutionDir)后面紧跟的是20240727_WPFMvvm,这看起来像是你想将$(SolutionDir)的值直接与这个文件夹名拼接起来,但通常$(SolutionDir)已经包含了解决方案的根目录,所以你可能不需要再显式地添加年份和项目名(除非它们实际上是解决方案目录下的一个子目录)。这里我假设20240727_WPFMvvm是解决方案目录下的一个子目录。
  4. XH.PrismLesson\XH.PrismModule\bin\Debug\net7.0-windows\:这是你希望将文件复制到的具体文件夹路径。它看起来像是你的解决方案中的一个项目(XH.PrismLesson)的子项目(XH.PrismModule)的特定构建配置(Debug)和目标框架(net7.0-windows)的输出目录。
  5. $(TargetFileName):是另一个宏,它只包含输出文件的文件名(不包括路径)。在你的场景中,这可能不是完全必要的,因为$(TargetPath)已经包含了完整的路径和文件名。但是,如果你的意图是确保文件名被正确复制(而不是覆盖整个路径),那么这个宏是有用的。然而,由于你的目标路径已经指定了详细的目录结构,通常你会想要直接使用$(TargetPath)而不是仅文件名。
  6. /y:是一个xcopy命令的参数,表示覆盖现有文件而不提示。
  7. ModulePath:是在文件扫描的时候,扫描此文件的所有dll进行注册

  1. 通过 Config 进行注册

APP.Config:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <!--name:modules
    type:引入wpf-->
    <section name="modules" type="Prism.Modularity.ModulesConfigurationSection, Prism.Wpf"/>
  </configSections>
  <modules>
    <!--assemblyFile:模块的程序集
    moduleName:模块的名称
    moduleType:typeof(AModule).AssemblyQualifiedName,就是模块的全名称-->
    <module assemblyFile = "XH.PrismModule.ModuleA.dll"
      moduleName = "AModule"
      moduleType = "XH.PrismModule.ModuleA.AModule, XH.PrismModule.ModuleA, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
  </modules>
</configuration>

IModuleCatalog类:

protected override IModuleCatalog CreateModuleCatalog()
{
    return new ConfigurationModuleCatalog();
}
  1. XML文件配置

XML代码:

<?xml version="1.0" encoding="utf-8" ?>
<p:ModuleCatalog xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                 xmlns:p="clr-namespace:Prism.Modularity;assembly=Prism.Wpf">
  <p:ModuleInfo ModuleName = "AModule" 
                ModuleType = "XH.PrismModule.ModuleA.AModule, XH.PrismModule.ModuleA, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
</p:ModuleCatalog>

IModuleCatalog类:

protected override IModuleCatalog CreateModuleCatalog()
{
    //return new XamlModuleCatalog(".\\ModuleConfig.xml");
    // 两个都可以
    return new XamlModuleCatalog("pack://application:,,,/XH.PrismModule;component/ModuleConfig.xml");
}
  1. 文件扫描方式注册(推荐)

IModuleCatalog类:

 protected override IModuleCatalog CreateModuleCatalog()
 {
     // 需要在bin/Debug 下有 ModulePath 目录,进行扫描
     return new DirectoryModuleCatalog()
     {
         // 配置将要扫描的目录
         ModulePath = ".\\ModulePath"
     };
 }

不过需要在当前文件夹下创建 ModulePath 文件夹,然后让其他Model 类库生成dll 放入到ModulePath 文件夹中。

模块懒加载

OnDemand:是否懒加载 默认是false

[Module(ModuleName ="AAA",OnDemand =true)]
public class AModule : IModule
{
    // 初始化
    public void OnInitialized(IContainerProvider containerProvider)
    {
        
    }

    // 注入
    public void RegisterTypes(IContainerRegistry containerRegistry)
    {
        containerRegistry.RegisterForNavigation<ViewA>();
    }
}

调用代码:

如果开启懒加载的话 需要手动加载

 public MainWindowViewModel(IRegionManager regionManager,IModuleManager moduleManager)
 {
     OpenCommand = new DelegateCommand(() =>
     {
         // 加载模块 OnDemand =true 如果开启懒加载的话 需要手动加载
         //moduleManager.LoadModule("AAA");
         regionManager.RequestNavigate("MainRegion","ViewA");
     });
 }

也可以在配置的时候,按需加载:

 // 配置模块注册
 protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
 {
     Type type = typeof(AModule);
     moduleCatalog.AddModule(new ModuleInfo
     {
         ModuleName = type.Name,
         ModuleType = type.AssemblyQualifiedName,
         // 标记Module按需加载 懒加载
         InitializationMode = InitializationMode.OnDemand,
     });
 }
模块动态加载

动态加载的意思是,程序启动的时候 ModulePath 文件中增加模块dll 也能加载成功。 热加载

动态加载类:

 public class DynamicLoadModule : ModuleCatalog
 {
     SynchronizationContext _context;

     /// <summary>
     /// Directory containing modules to search for.
     /// </summary>
     public string ModulePath { get; set; }

     public DynamicLoadModule(string modulePath)
     {
         _context = SynchronizationContext.Current;

         ModulePath = modulePath;

         // we need to watch our folder for newly added modules
         FileSystemWatcher fileWatcher = new FileSystemWatcher(ModulePath, "*.dll");
         fileWatcher.Created += FileWatcher_Created;
         fileWatcher.EnableRaisingEvents = true;
     }

     /// <summary>
     /// Rasied when a new file is added to the ModulePath directory
     /// </summary>
     void FileWatcher_Created(object sender, FileSystemEventArgs e)
     {
         if (e.ChangeType == WatcherChangeTypes.Created)
         {
             LoadModuleCatalog(e.FullPath, true);
         }
     }

     /// <summary>
     /// Drives the main logic of building the child domain and searching for the assemblies.
     /// </summary>
     protected override void InnerLoad()
     {
         LoadModuleCatalog(ModulePath);
     }

     void LoadModuleCatalog(string path, bool isFile = false)
     {
         if (string.IsNullOrEmpty(path))
             throw new InvalidOperationException("Path cannot be null.");

         if (isFile)
         {
             if (!File.Exists(path))
                 throw new InvalidOperationException(string.Format("File {0} could not be found.", path));
         }
         else
         {
             if (!Directory.Exists(path))
                 throw new InvalidOperationException(string.Format("Directory {0} could not be found.", path));
         }

         AppDomain childDomain = this.BuildChildDomain(AppDomain.CurrentDomain);

         try
         {
             List<string> loadedAssemblies = new List<string>();

             var assemblies = (
                                  from Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()
                                  where !(assembly is System.Reflection.Emit.AssemblyBuilder)
                                     && assembly.GetType().FullName != "System.Reflection.Emit.InternalAssemblyBuilder"
                                     && !String.IsNullOrEmpty(assembly.Location)
                                  select assembly.Location
                              );

             loadedAssemblies.AddRange(assemblies);

             Type loaderType = typeof(InnerModuleInfoLoader);
             if (loaderType.Assembly != null)
             {
                 var loader = (InnerModuleInfoLoader)childDomain.CreateInstanceFrom(loaderType.Assembly.Location, loaderType.FullName).Unwrap();
                 loader.LoadAssemblies(loadedAssemblies);

                 //get all the ModuleInfos
                 ModuleInfo[] modules = loader.GetModuleInfos(path, isFile);

                 //add modules to catalog
                 this.Items.AddRange(modules);

                 //we are dealing with a file from our file watcher, so let's notify that it needs to be loaded
                 if (isFile)
                 {
                     LoadModules(modules);
                 }
             }
         }
         finally
         {
             AppDomain.Unload(childDomain);
         }
     }

     /// <summary>
     /// Uses the IModuleManager to load the modules into memory
     /// </summary>
     /// <param name="modules"></param>
     private void LoadModules(ModuleInfo[] modules)
     {
         if (_context == null)
             return;

         IModuleManager manager = ServiceLocator.Current.GetInstance<IModuleManager>();

         _context.Send(new SendOrPostCallback(delegate (object state)
         {
             foreach (var module in modules)
             {
                 manager.LoadModule(module.ModuleName);
             }
         }), null);
     }

     /// <summary>
     /// Creates a new child domain and copies the evidence from a parent domain.
     /// </summary>
     /// <param name="parentDomain">The parent domain.</param>
     /// <returns>The new child domain.</returns>
     /// <remarks>
     /// Grabs the <paramref name="parentDomain"/> evidence and uses it to construct the new
     /// <see cref="AppDomain"/> because in a ClickOnce execution environment, creating an
     /// <see cref="AppDomain"/> will by default pick up the partial trust environment of
     /// the AppLaunch.exe, which was the root executable. The AppLaunch.exe does a
     /// create domain and applies the evidence from the ClickOnce manifests to
     /// create the domain that the application is actually executing in. This will
     /// need to be Full Trust for Composite Application Library applications.
     /// </remarks>
     /// <exception cref="ArgumentNullException">An <see cref="ArgumentNullException"/> is thrown if <paramref name="parentDomain"/> is null.</exception>
     protected virtual AppDomain BuildChildDomain(AppDomain parentDomain)
     {
         if (parentDomain == null) throw new System.ArgumentNullException("parentDomain");

         Evidence evidence = new Evidence(parentDomain.Evidence);
         AppDomainSetup setup = parentDomain.SetupInformation;
         return AppDomain.CreateDomain("DiscoveryRegion", evidence, setup);
     }


     private class InnerModuleInfoLoader : MarshalByRefObject
     {
         [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")]
         internal ModuleInfo[] GetModuleInfos(string path, bool isFile = false)
         {
             Assembly moduleReflectionOnlyAssembly =
                 AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies().First(
                     asm => asm.FullName == typeof(IModule).Assembly.FullName);

             Type IModuleType = moduleReflectionOnlyAssembly.GetType(typeof(IModule).FullName);

             FileSystemInfo info = null;
             if (isFile)
                 info = new FileInfo(path);
             else
                 info = new DirectoryInfo(path);

             ResolveEventHandler resolveEventHandler = delegate (object sender, ResolveEventArgs args) { return OnReflectionOnlyResolve(args, info); };
             AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += resolveEventHandler;
             IEnumerable<ModuleInfo> modules = GetNotAllreadyLoadedModuleInfos(info, IModuleType);
             AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve -= resolveEventHandler;

             return modules.ToArray();
         }

         private static IEnumerable<ModuleInfo> GetNotAllreadyLoadedModuleInfos(FileSystemInfo info, Type IModuleType)
         {
             List<FileInfo> validAssemblies = new List<FileInfo>();
             Assembly[] alreadyLoadedAssemblies = AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies();

             FileInfo fileInfo = info as FileInfo;
             if (fileInfo != null)
             {
                 if (alreadyLoadedAssemblies.FirstOrDefault(assembly => String.Compare(Path.GetFileName(assembly.Location), fileInfo.Name, StringComparison.OrdinalIgnoreCase) == 0) == null)
                 {
                     var moduleInfos = Assembly.ReflectionOnlyLoadFrom(fileInfo.FullName).GetExportedTypes()
                     .Where(IModuleType.IsAssignableFrom)
                     .Where(t => t != IModuleType)
                     .Where(t => !t.IsAbstract).Select(t => CreateModuleInfo(t));

                     return moduleInfos;
                 }
             }

             DirectoryInfo directory = info as DirectoryInfo;

             var files = directory.GetFiles("*.dll").Where(file => alreadyLoadedAssemblies.
                 FirstOrDefault(assembly => String.Compare(Path.GetFileName(assembly.Location), file.Name, StringComparison.OrdinalIgnoreCase) == 0) == null);

             foreach (FileInfo file in files)
             {
                 try
                 {
                     Assembly.ReflectionOnlyLoadFrom(file.FullName);
                     validAssemblies.Add(file);
                 }
                 catch (BadImageFormatException)
                 {
                     // skip non-.NET Dlls
                 }
             }

             return validAssemblies.SelectMany(file => Assembly.ReflectionOnlyLoadFrom(file.FullName)
                                         .GetExportedTypes()
                                         .Where(IModuleType.IsAssignableFrom)
                                         .Where(t => t != IModuleType)
                                         .Where(t => !t.IsAbstract)
                                         .Select(type => CreateModuleInfo(type)));
         }


         private static Assembly OnReflectionOnlyResolve(ResolveEventArgs args, FileSystemInfo info)
         {
             Assembly loadedAssembly = AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies().FirstOrDefault(
                 asm => string.Equals(asm.FullName, args.Name, StringComparison.OrdinalIgnoreCase));
             if (loadedAssembly != null)
             {
                 return loadedAssembly;
             }

             DirectoryInfo directory = info as DirectoryInfo;
             if (directory != null)
             {
                 AssemblyName assemblyName = new AssemblyName(args.Name);
                 string dependentAssemblyFilename = Path.Combine(directory.FullName, assemblyName.Name + ".dll");
                 if (File.Exists(dependentAssemblyFilename))
                 {
                     return Assembly.ReflectionOnlyLoadFrom(dependentAssemblyFilename);
                 }
             }

             return Assembly.ReflectionOnlyLoad(args.Name);
         }

         [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")]
         internal void LoadAssemblies(IEnumerable<string> assemblies)
         {
             foreach (string assemblyPath in assemblies)
             {
                 try
                 {
                     Assembly.ReflectionOnlyLoadFrom(assemblyPath);
                 }
                 catch (FileNotFoundException)
                 {
                     // Continue loading assemblies even if an assembly can not be loaded in the new AppDomain
                 }
             }
         }

         private static ModuleInfo CreateModuleInfo(Type type)
         {
             string moduleName = type.Name;
             List<string> dependsOn = new List<string>();
             bool onDemand = false;
             var moduleAttribute = CustomAttributeData.GetCustomAttributes(type).FirstOrDefault(cad => cad.Constructor.DeclaringType.FullName == typeof(ModuleAttribute).FullName);

             if (moduleAttribute != null)
             {
                 foreach (CustomAttributeNamedArgument argument in moduleAttribute.NamedArguments)
                 {
                     string argumentName = argument.MemberInfo.Name;
                     switch (argumentName)
                     {
                         case "ModuleName":
                             moduleName = (string)argument.TypedValue.Value;
                             break;

                         case "OnDemand":
                             onDemand = (bool)argument.TypedValue.Value;
                             break;

                         case "StartupLoaded":
                             onDemand = !((bool)argument.TypedValue.Value);
                             break;
                     }
                 }
             }

             var moduleDependencyAttributes = CustomAttributeData.GetCustomAttributes(type).Where(cad => cad.Constructor.DeclaringType.FullName == typeof(ModuleDependencyAttribute).FullName);
             foreach (CustomAttributeData cad in moduleDependencyAttributes)
             {
                 dependsOn.Add((string)cad.ConstructorArguments[0].Value);
             }

             ModuleInfo moduleInfo = new ModuleInfo(moduleName, type.AssemblyQualifiedName)
             {
                 InitializationMode =
                     onDemand
                         ? InitializationMode.OnDemand
                         : InitializationMode.WhenAvailable,
                 Ref = type.Assembly.CodeBase,
             };
             moduleInfo.DependsOn.AddRange(dependsOn);
             return moduleInfo;
         }
     }
 }

 /// <summary>
 /// Class that provides extension methods to Collection
 /// </summary>
 public static class CollectionExtensions
 {
     /// <summary>
     /// Add a range of items to a collection.
     /// </summary>
     /// <typeparam name="T">Type of objects within the collection.</typeparam>
     /// <param name="collection">The collection to add items to.</param>
     /// <param name="items">The items to add to the collection.</param>
     /// <returns>The collection.</returns>
     /// <exception cref="System.ArgumentNullException">An <see cref="System.ArgumentNullException"/> is thrown if <paramref name="collection"/> or <paramref name="items"/> is <see langword="null"/>.</exception>
     public static Collection<T> AddRange<T>(this Collection<T> collection, IEnumerable<T> items)
     {
         if (collection == null) throw new System.ArgumentNullException("collection");
         if (items == null) throw new System.ArgumentNullException("items");

         foreach (var each in items)
         {
             collection.Add(each);
         }

         return collection;
     }
 }

在注册Module的时候,创建动态加载:

 protected override IModuleCatalog CreateModuleCatalog()
 {
     // 动态加载
     return new DynamicLoadModule(".\\ModulePath");
 }

总结

  1. 已实现,直接注入使用

IUnityContainer、IContainerExtension、IContainerProvider、IContainerRegistry、IModuleCatalog、IDialogService

IModuleManager、IRegionManager、IEventAggregator、IRegionBehaviorFactory、IRegionNavigationJournalEntry

IRegionNavigationJournal

  1. 继承实现

IDialogWindow、IDialogAware、IActiveAware、INavigationAware、IConfirmNavigationRequest、IRegionMemberLifetime

IJournalAware、IModule

  • 12
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: WPF Prism框架是一个面向对象的框架,用于开发模块化、可扩展的WPF应用程序,它基于MVVM设计模式和依赖注入技术。该框架的主要目的是能够轻松地实现可插拔的模块,公共的服务、组件和工具类的共享,同时也提供了灵活的路由、事件聚合、模块加载、导航和命令处理等机制。使用WPF Prism框架可以快速地开发出灵活的WPF应用程序,从而提高代码质量和开发效率,减少代码的维护难度。 WPF Prism框架具有以下的特点: 1. 扩展性:可以轻松地添加新的模块、服务和组件,同时也可以快速替换现有的组件。 2. 可重用性:提供了丰富的公共组件、服务和工具类,从而可以提高代码的可重用性。 3. 灵活性:提供了灵活的路由、事件聚合、模块加载和导航等机制,能够更好地满足应用程序的需求。 4. 易用性:提供了一套完整的MVVM设计模式和依赖注入技术的实践方案,从而能够更好地组织应用程序的逻辑。 总之,WPF Prism框架是一个强大的工具,能够让我们更好地开发WPF应用程序,提高代码质量和开发效率,实现可插拔的模块化和可扩展性,同时也具有灵活性和易用性。 ### 回答2: WPF Prism框架是一种面向MVVM模式的开源框架,它帮助开发人员使用模块化的方式构建可扩展、可重用和易于维护的WPF应用程序。该框架主要由Microsoft和模式仲裁者团队开发和维护,它借鉴了许多现代的软件开发理念,比如IoC容器、依赖注入和事件聚合器等。 WPF Prism框架的核心思想是将应用程序分解为许多可独立维护和扩展的模块。这些模块可以基于业务逻辑、UI、数据或任何其他特征进行分组。在该框架中,模块由各种名为“组件”的构建块组成。这些组件包括视图(View)、视图模型(ViewModel)、服务(Service)、模型(Model)等。通过基于这些组件的开发,可以实现具有高度可伸缩性和可重用性的应用程序。 为了实现这种模块化开发和组件化架构,WPF Prism框架提供了一些重要的工具和特性。例如,在该框架中可以使用依赖注入容器(如Unity)来管理组件及其依赖关系。此外,该框架还提供了一些基于事件的消息机制,可方便地实现模块间的交互和通信。 总体来说,WPF Prism框架是一种利用开源技术实现企业级应用程序开发的最佳选择。它具有良好的模块化、组件化和可扩展性特性,可以在实现复杂WPF应用程序时提高开发效率和代码质量。 ### 回答3: WPF Prism是一个基于WPF框架,它为大型应用程序提供了一种组织、设计和部署的方式。它旨在帮助开发者构建可扩展、可维护和可测试的WPF应用程序。 WPF Prism采用了面向模块的编程思想,它将整个应用程序划分为多个模块,每个模块都包含自己的逻辑和用户界面。这种模块化设计使得应用程序易于扩展和维护,同时也简化了开发流程。 WPF Prism同时提供了一组强大的工具和功能,如依赖注入、命令模式和事件聚合等,这些功能让WPF应用程序更加易于开发和测试。它还提供了一个强大的导航和区域管理系统,开发者可以使用这些系统来管理不同部分的用户界面和功能。 总之,WPF Prism是一个优秀的框架,它为开发者提供了全面的工具和功能,使得构建WPF应用程序变得更加容易和高效。它的结构良好、可扩展性强,而且可以充分利用WPF的强大功能。无论是大型企业应用程序还是小型桌面应用程序,WPF Prism都是一个理想的选择。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值