Nuget包地址: https://www.nuget.org/packages/QuickFramework.Wpf/
用vs的nuget搜索包时,请勾选“包括预发行版本”,搜索关键字:QuickFramework.Wpf
QQ交流群:196554374
注:本框架要求.net framework 4.6.1或以上版本,暂不支持.net core和.net 5,6。编译源码需要vs2019或以上版本,当第一次编译提示错误时,在解决方案上右键,点击“清理解决方案”,然后再次编译,即可成功
从事了10多年的.net和WPF开发,用过各种各样的设计模式和框架,包括Prism,Abp等框架,自己也曾根据自己的理解搭建过一些开发框架。各种模式和框架有它的优点,也有缺点,这是很正常的事情,因为众口难调。有些开发者不喜欢层次太多,太复杂,过于考虑扩展性的框架,他们关注点往往是快速开发,希望框架提供很多能直接用的轮子,而不关注框架的扩展性。有些开发者又需要模块和层次足够多,为将来的扩展性留有余地,他们不太在意复杂度和开发速度,而更在于架构的扩展性和灵活性。因此没有一个框架能说是完美的,能满足如此多的开发人员的不同需求。基于多年的经验和走过的路,我常常在想,WPF开发应该用那种架构最为合适,应该提供哪些轮子能使开发变的容易。于是针对开发中的种种痛点,逐渐形成了自己的思路,决定基于已有的一些经验打造一个新的框架。这个新框架同样不可能满足所有的需求,必须基于一些特定的设计目标和设计原则。
Quick框架原则之一是要上手简单,快速开发,尽可能的提供轮子解决WPF开发中的痛点。原则之二是要求开发者遵循框架,但不会将方方面面框死,应该给使用者留有很大自由度,因此不会像领域驱动那样,强迫开发者去用DDD。这个框架不会满足所有人的需求,希望理解它,愿意用的人用之,欢迎提出宝贵意见,不喜欢用的人也不要喷,每个框架的关注重点不一样,还是我上面说的众口难调。即使像Abp框架这样看似强大的框架,也有很多人不买账,它的缺点恰恰就是过于强大,过于解耦,太重了,忽视了开发的便利性。
另外,本框架使用大量第三方优秀类库作为支撑,因为我认为没必要重复造轮子,很多轮子经过多年的打磨,付出了大量的精力。如果像Prism那样,什么都自己做,我觉得一是没有时间去造,二也造不好。
上图是框架核心的组成,为了便利性和架构的平衡,并没有拆解的太细,而是相对集成,降低复杂度。因此把轻量的Dapper ORM也放到了核心里,而重量级的Entity Framework则单独提供了一个Dll来支持,根据需要进行引用。
主要是对一些常用的.Net基础类进行了扩展,比如byte[]转md5,转hex字符串等等, 常用的有判断字符串IsNullOrEmpty(),判断集合IsNullOrEmpty()等,还包括一些常用的TaskScheduler。
模块化和依赖注入是整个框架的核心,尽管有些初学者难以理解这两个概念,但是我认为框架有必要使用这两个机制。模块化有助于架构的清晰,合理,规范化,而依赖注入能提高开发效率和解耦。这里不是为了高大上而用这两个机制,而是为了我们合理架构和快速开发,确实需要模块化和依赖注入机制的支持。对于模块化,参考了Abp框架,它的一个很大优点是模块依赖的概念,解决了依赖注入时因先后顺序不可控导致的问题。对于依赖注入机制,则使用了目前最为流行的Autofac,之所以选择Autofac,因为它提供了足够强大的特性和极高的效率,在业界有很好的口碑,它发展了多年,迭代了很多版本,非常成熟。我认为没有必要像prism那样去自己打造一个并不完善的IOC。
Autofac帮助文档:https://autofac.readthedocs.io/en/latest/getting-started/index.html
采用流行的Serilog提供支持,帮助文档:https://github.com/serilog/serilog/wiki/Getting-Started
配置文件机制也有各种各样的实现方法,本框架实现基于json的,可读写的配置系统,方便之处在于,标记一个类即可写入配置,并自动全局注入。
通常来说一个WPF应用程序只访问一个数据库,在访问多个数据库时往往架构比较混乱,本框架提供了一个高度解耦的多数据库机制
Dapper作为轻量级的数据库访问类库,深受广大开发者欢迎,本框架封装了一个强大的基于Dapper的ORM库,很容易在框架中进行数据库操作。当然,喜欢Entity Framework的开发者也可以继续使用EF,二者不冲突,甚至可以混合使用(不推荐)。
Local Storage的好处在于,可以随时随地的存储数据,极大提高了便利性,因为很多时候,一些配置和临时数据记录并不会都存放于数据库,开发者也不愿意专门为这些数据单独写存储机制。
本框架使用AutoMapper来做对象映射。历史经验告诉我们,很多业务场景下,Entity与表现层的Dto分离是必要的,否则必将导致业务混乱。而Entity与Dto的转换又是一个非常繁琐的问题,AutoMapper恰恰就是来解决对象转换问题的。
AutoMapper帮助文档:https://docs.automapper.org/en/stable/index.html
包括应用单一实例设置,重复启动第二个实例时自动激活第一个,全局异常捕获,崩溃自动写日志等功能。
界面多语言切换有很多种解决方案,本框架还是采用WPF的DynamicResource机制,因为这个机制在Xaml中使用时是最简单的
一个实现订阅发布机制的消息总线对于应用来说是非常有用的,提高了解决问题的能力,也能帮助实现一些场景下的解耦。本框架底层以DevExpress.Mvvm 的Messenger为基础实现了消息的发布订阅。这里需要特别说明的是,这类消息机制无一例外都是通过全局中间件来进行通信的,因此向全局容器订阅消息时,将把订阅者的引用注册到全局容器中,尽管使用了弱引用机制,但仍广泛存在垃圾回收不及时的问题。因此这里有两条建议:一是尽量避免在临时对象中注册消息,建议在常驻对象中订阅消息,比如主窗口类,单例服务等。二是如果确需在临时对象中注册消息,记得在对象不用时注销消息订阅,这样能使得引用立即被释放。比如在弹出一个窗口中订阅了消息,那么在窗口关闭时应该注销订阅。
对于UI控件样式的增强,确实是一个耗费巨大精力和长耗时的工作,因此本框架没有做过多的控件样式增强,开发者可以使用任何第三方UI。Quick框架本身提供了一套简易的UI皮肤,用户可以参照源码,很容易进行重写。
本框架提供了一些增强的控件,如:AutoCompleteComboBox,AutoFitTextControl,FilePicker,IPAddressControl,LoadingBox,PageBar,PageGrid,PageItemsControl,RadioSelector,SpaceStackPanel,SpinnerControl等等,同时也增加了对TextBox输入的限制功能。
本框架使用了DevExpress.Mvvm提供的DXEvent, DXBinding, DXCommand三大神器,但并没有使用Dev的WPF框架。本框架以这三个扩展为基础,展示了与众不同的MVVM思路,极大提高了MVVM下开发的便利性,这也是本框架的一个非常大的优势,解决各种MVVM开发中的痛点。
写转换器是令很多WPF开发人员反感的一件事,本框架提供了大量内置转换器,提供了QBinding,UniversalConverter两个强大的语法,再加上Dev的DXBinding的加持,基本告别了写转换器的历史。如果必须要写转换器,那么框架提供了Format机制,使得写转换器非常简单,一个函数即可。
MessageBox是WPF开发中一个很重要的角色,本框架重写了MessageBox,提供了设置选项,可以从系统,Handy Control和本框架自己实现的消息框中任选一种。由于Handy Control的消息框存在加载慢和按钮显示延迟问题,不建议使用。
Entity Framework作为一个重量级的数据库访问框架,深受一些开发者喜爱,本框架对EF的支持主要在于提供了ViewModel基类,更规范和方便的调用EF,同时实现了自动UI下的对EF的无感调用。
采用ViewModel属性标记的方式,由ViewModel自动生成UI,极大的减少了Xaml代码量。对于RadioButton组,枚举的绑定问题,框架也提供了完善的解决方案。除此之外,允许开发者自定义标记和编写控件生成器。
本框架的核心目标是提升开发效率,同时兼顾架构,因此这是一个开发效率与架构平衡的框架,并不会像很多框架那样,为了架构而牺牲开发效率。本节重点介绍提升效率的功能特点,如果你看完本节觉得这些功能确实解决了你的问题,提升了开发效率,那么你可以试用一下这个框架。
string扩展
// ------------原写法------------
string name = GetUserName(userId);
if( string.IsNullOrEmpty(name) )
{
throw new Exception("The user is not found.");
}
// ++++++++++++扩展写法+++++++++++
if( name.IsNullOrEmpty() )
{
}
有些开发者说并没有节省多少代码,但是写多了之后,就会发现用扩展写法还是方便的多,下面列出一些常用的:
str.IsNullOrEmpty();
str.IsNullOrWhiteSpace();
string[] lines = str.ReadAllLines();
string md5 = str.ToMd5();
byte[] bytes = str.ToHexBytes();
string strLeft = str.Left(5);
string strRight = str.Right(5);
byte[] utf8 = str.GetBytes(Encoding.UTF8);
byte[]扩展
byte[] data;
string str = data.ToHexString();
string md5 = data.ToMd5UpperString();
IEnumerable扩展
// 原写法
List<User> users = GetUsers();
if( user == null || user.Count == 0 )
{
throw new Exception("The users are empty!");
}
// 扩展写法
if( users.IsNullOrEmpty() )
{
}
还有很多其他扩展,不全部列出了
很多新接触 WPF的开发人员,对于MVVM开发特别不适应,他们会觉得各种繁琐,甚至对该模式有所抵触,该节着重介绍本框架的MVVM的理念
为了普通类支持绑定,WPF要求类实现INotifyPropertyChanged 接口,每个属性的Set都要写Notify,这是很多开发人员很反感的。
// ------------原写法1------------
private string _name;
public string Name
{
get { return _name; }
set
{
if( _name != value )
{
_name = value;
NotifyPropertyChanged(nameof(Name));
}
}
}
// ------------改进写法2------------
private string _name;
public string Name
{
get => _name;
set => SetProperty(ref _name, value);
}
// ++++++++++++Quick框架写法+++++++++++
public string Name { get; set; }
我们知道按钮的执行要绑定到ViewModel,需要在ViewModel中定义ICommand,很多开发人员对此都十分反感,界面几十个按钮,就要写几十遍这个东西
// ------------原写法------------
<Button Command="{Binding Path=SearchCommand}" />
private ICommand _searchCommand;
public ICommand SearchCommand
{
get
{
if( _searchCommand == null )
{
_searchCommand = new RelayCommand(DoSearch(), null);
}
return _searchCommand;
}
}
public void DoSearch(object param)
{
}
// ++++++++++++Quick框架写法+++++++++++
<Button Command="{DXCommand DoSearch()}" />
令WPF开发人员困惑的是,有Command属性的控件,我们尚能绑定到ViewModel,而很多控件并没有Command属性。之前的做法要不用EventToCommand之类的附加属性来解决,要不用Interaction框架。这里特别说明,本人非常反感使用Interaction机制,极大了降低了开发效率,我相信很多开发者跟我也一样。
// ------------原写法1------------
//缺点:写法繁琐,事件是字符串,无法智能提示,还要在ViewModel定义一个Command,每个Xaml还要声明ext命名空间
<ListBox ext:EventToCommand.Event="SelectionChanged" ext.EventToCommand.Handler="{Binding SelChangedCommand}" />
// ------------原写法2------------
//缺点:写法非常繁琐
<ListBox>
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<i:InvokeCommandAction Command="{Binding SelChangedCommand}" />
</i:EventTrigger>
</i:EventTrigger>
</ListBox>
// ++++++++++++Quick框架写法+++++++++++
<ListBox SelectionChanged="{DXEvent SelectionChange()}" />
<Button Click="{DXEvent DoSearch()}" />
有人提出这跟在View.cs里写事件处理有什么区别呢,这里的区别在于,绑定的函数必须在ViewModel里,保持了ViewModel的业务完整性,而ViewModel无法访问UI,保持了View和ViewModel的隔离,也是符合MVVM**的。很多人的误区在于,ViewModel只有暴露ICommand才是MVVM,UI只有绑定了ICommand才是MVVM,这个**是不对的,对于ViewModel而言,只要不访问UI,保证了业务完整性即可,不论是暴露ICommand还是暴露函数,都是没有问题的。回到最开始说的问题,如果在view.cs里写了处理函数,那么将难以避免将一些业务逻辑写在view.cs里,导致本应写在ViewModel里的功能分散到了view.cs里,破坏了ViewModel的完整性,导致ViewModel依赖View.cs,造成耦合。
(1) 在界面绑定属性时,很多时候不能直接绑定,每遇到要写转换器,也是一件让开发人员头疼的事情,Quick框架提供了多种方式解决这个问题,详见ConvertersDemo源码
//假设以下需求:根据ViewModel中网络状态,界面控件改变颜色
public enum NetworkStatus
{
Disconnected,
Connecting,
Connected,
}
public class MainViewModel : ViewModelBase
{
public NetworkStatus NetworkStatus { get; set; }
}
// ----------------------------原写法-------------------------------
//由于ViewModel中数据类型是一个枚举,那么我们需要定义一个转换器,将枚举转颜色
public class NetworkStatusToBrushConverter : IValueConverter
{
public static NetworkStatusToBrushConverter Default { get; } = new NetworkStatusToBrushConverter();
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value == null)
{
return Brushes.Gray;
}
try
{
NetworkStatus status = (NetworkStatus)value;
if( status == NetworkStatus.Connecting )
{
return Brushes.Orange;
}
if( status == NetworkStatus.Connected )
{
return Brushes.Green;
}
}
catch
{
}
return Brushes.Gray;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException()
}
}
<Rectangle Fill="{Binding Path=NetworkStatus, Converter={x:Static cnv:NetworkStatusToBrushConverter.Default}}" />
//或者定义一个资源
<cnv:NetworkStatusToBrushConverter x:Key="NetworkStatusToBrushConverter" />
<Rectangle Fill="{Binding Path=NetworkStatus, Converter={StaticResource NetworkStatusToBrushConverter}}" />
// +++++++++++++++++++++++++Quick框架写法+++++++++++++++++++++++++++++++++
<Rectangle Fill="{QBinding Path=NetworkStatus, ConverterParameter=[Gray][Connecting:Orange][Connected:Green]}" />
//语法含义:默认为Gray,Connecting为橙色,Connected为绿色
可以看到,通过使用Quick框架,无需定义转换器,只用一行代码就实现了同样的效果,大大提升开发效率
(2) 开发人员遇到需要绑定多个属性时,也经常不想去写转换器,觉得非常繁琐,Quick框架提供了以下解决方案,详见SimpleMvvmDemo源码
以下是需要绑定的ViewModel,需求是在界面上显示图片的尺寸描述,有两种单位制,一个是像素,一个是厘米,那么我们的界面需要同时绑定Image和UnitIndex,这样在图片或单位变化时,尺寸描述都可以自动变化
public class MainWindowViewModel : QValidatableBase
{
public BitmapImage Image { get; set; } //图片
public int UnitIndex { get; set; } = 0; //单位下拉框的选择索引,0为像素,1为厘米
public string GetImageSize()
{
if (Image == null)
{
return null;
}
if (UnitIndex == 0)
{
return $"{Image.PixelWidth} x {Image.PixelHeight}";
}
else
{
double widthInch = (double)Image.PixelWidth / (double)Image.DpiX;
double heightInch = (double)Image.PixelHeight / (double)Image.DpiY;
double widthCm = widthInch * 2.54;
double heightCm = heightInch * 2.54;
return $"{widthCm:F02}厘米 x {heightCm:F02}厘米";
}
}
}
// ----------------------------原写法-------------------------------
//定义MultiConverter,这里只写出大致代码
public class ImageToSizeDesConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
return null;
}
}
//XAML代码:
<cnv:ImageToSizeDesConverter x:Key="ImageToSizeDesConverter" />
<TextBlock>
<TextBlock.Text>
<MultiBinding Converter="{StaticResource ImageToSizeDesConverter}"
<Binding Path="Image" />
<Binding Path="UnitIndex" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
// +++++++++++++++++++++++++Quick框架写法1+++++++++++++++++++++++++++++++++
<TextBlock Text="{DXBinding 'Triggers(Image, UnitIndex).GetImageSize()'}" />
//注释:指定触发属性为Image, UnitIndex,二者中有任何一个变化时,调用GetImageSize()函数获取返回值,
//这样做的好处是不必写转换器类了
// +++++++++++++++++++++++++Quick框架写法2+++++++++++++++++++++++++++++++++
/* 在ViewModel中编写颜色转换等UI代码实际上是违背原则的,因此不推荐1的写法,
Quick框架提供了一个全局的IFormat来集中编写转换函数,使得UI代码和业务代码分离
首先定义一个Format类,继承自IFormat,框架启动时将自动注入单例,应用所有的
转换函数都写在这个类里,这样比写WPF的Converter要简单的多,同时也独立于ViewModel,
使得ViewModel不含界面代码*/
public class Format : IFormat
{
public string GetImageSize(Image image, int unitIndex)
{
if (image == null)
{
return null;
}
if (unitIndex == 0)
{
return $"{image.PixelWidth} x {image.PixelHeight}";
}
else
{
double widthInch = (double)image.PixelWidth / (double)image.DpiX;
double heightInch = (double)image.PixelHeight / (double)image.DpiY;
double widthCm = widthInch * 2.54;
double heightCm = heightInch * 2.54;
return $"{widthCm:F02}厘米 x {heightCm:F02}厘米";
}
}
public string ToDataSizeDes(long size)
{
double fSize = (double)size / 1024.0;
string unit = "KB";
if (fSize >= 1024)
{
fSize /= 1024.0;
unit = "MB";
}
if (fSize >= 1024)
{
fSize /= 1024.0;
unit = "GB";
}
return fSize.ToString("F02") + " " + unit;
}
}
//XAML代码
<TextBlock Text="{DXBinding 'Format.GetImageSize(Image, UnitIndex)'}" />
在MVVM模式下弹出窗口,确实是一个难题,于是有了各种各样的解决方案,最为代表性的是IDialogService这种解决方式,然后又带来了ViewModel如何关窗的问题,如何拦截窗口关闭问题,如何设置初始窗口大小问题,如何控制窗口最大化,最小化按钮等等问题。解决方法不胜枚举,但很多都是绕了一大圈,要写一大堆代码才能实现。Quick框架的解决方案是回归原始,界面的问题由view.cs代码解决,业务的问题由ViewModel.cs解决
//我们用最原始的写法写事件响应函数
<Button Content="登陆" Click="btnLogin_Click" Width="100" Margin="10,0,0,0" />
public partial class MainWindow : Window
{
private readonly MainWindowViewModel _vm;
public MainWindow(MainWindowViewModel vm)
{
InitializeComponent();
_vm = vm;
this.DataContext = _vm;
}
//在按钮的响应函数中直接弹窗即可,但ViewModel的交互逻辑都在ViewModel中处理
private void btnLogin_Click(object sender, RoutedEventArgs e)
{
LoginWindow loginWnd = QServiceProvider.GetService<LoginWindow>();
loginWnd.Owner = this;
if (loginWnd.ShowDialog().Value)
{
//调用ViewModel的公开函数,不可在View.cs中写业务代码
_vm.SetLoginResult(loginWnd.VM);
}
}
}
试想一个需求场景:界面上有一个输入框和一个按钮,点击按钮之后, ViewModel执行搜索,搜索执行完毕之后,界面焦点需要回到输入框,并选中之前输入的关键词。这个需求令很多开发者无从下手,因为我们常用的方法是,界面Button->Command->ViewModel.Command->结束。由于不能在ViewModel中操作界面,因此输入焦点如何返回,输入的内容如何选中?
这里不再列举其他框架的解决方案,直接写出Quick框架的解决方式
<Button Content="搜索" Click="btnSearch_Click" Width="100" Margin="10,0,0,0" />
//同样的,我们用传统方式给按钮添加事件响应
public partial class MainWindow : Window
{
private readonly MainWindowViewModel _vm;
public MainWindow(MainWindowViewModel vm)
{
InitializeComponent();
_vm = vm;
this.DataContext = _vm;
}
private void btnSearch_Click(object sender, RoutedEventArgs e)
{
//ViewModel提供公开函数
_vm.DoSearch();
//操作界面焦点和输入全选
tbKeywork.Focus();
tbKeywork.SelectAll();
}
}
我相信一些人看了以上两节的代码,马上会提出反对意见:你这不是MVVM,你在View.cs里写代码了,你在View里添加事件处理了,你没有用Command。作为一个有10年WPF开发经验的人,我非常理解他们的意见,因为这也是我曾经的认知。我想说的是,MVVM是一种**,而不是某种固定的代码格式。ViewModel的职责是负责整体业务处理,并提供接口对外交互,接口的方式不仅限于ICommand,还包括公有函数,事件等。ViewModel是否合格,我们只需要看以下几点:
- ViewModel是否业务完整?
- ViewModel是否能独立跑通单元测试?
- ViewModel是否操作UI了?
- ViewModel能否适配另一个UI?
对照以上几条原则去检查上面的做法是没有问题的。因为ViewModel暴露了公开函数去跟UI交互,保持了自身业务的完整性和独立性。MainWindow.cs没有任何跟业务有关的代码,只是界面辅助代码和对ViewModel的接口函数调用而已。**唯一的缺点在于:框架本身没有限制开发者在view.cs里写业务代码,只能靠开发人员自我约束。**以上面的例子为例,如果_vm.DoSearch()的实现代码写在btnSearch_Click()里也不会报错,但这显然就破坏了ViewModel的完整性,回归到MVP开发的老模式上去了。本来想在ViewModel上提一层接口出来,阻止View直接看到ViewModel,从而避免在View.cs里写业务代码,但反复考虑之后,这会增加开发复杂度,违背Quick的初衷,同时考虑到这种情况是非常少,因此没有加接口机制。
我们知道WPF的动画是View层面的机制,我们常常有需求:ViewModel执行完某个操作后,让界面发起一段动画。详见SimpleMvvmDemo源码
//在ViewModel里注定义一个事件,某个操作完成后,执行该事件
public class MainWindowViewModel : QValidatableBase
{
public event EventHandler DownloadFinished;
public async void Download()
{
//延时模拟
await Task.Wait(2000);
DownloadFinished?.Invoke();
}
}
//在View.cs里注册事件
public partial class MainWindow : Window
{
private readonly MainWindowViewModel _vm;
public MainWindow(MainWindowViewModel vm)
{
InitializeComponent();
_vm = vm;
_vm.DownloadFinished += _vm_DownloadFinished;
this.DataContext = _vm;
}
//ViewModel下载完图片后,走一个界面动画,通过注册ViewModel的事件来实现解耦
private void _vm_DownloadFinished(object sender, EventArgs e)
{
DoubleAnimation animation = new DoubleAnimation();
animation.From = -500;
animation.Duration = TimeSpan.FromSeconds(1);
animation.To = 0;
animation.FillBehavior = FillBehavior.HoldEnd;
animation.EasingFunction = new CircleEase();
//imgTranslateTransform为界面定义的元素名称
imgTranslateTransform.BeginAnimation(TranslateTransform.XProperty, animation);
}
}
//XAML代码
<Button Content="下载图片" Click="{DXEvent Download()}" />
<Image Source="{Binding Path=Image}">
<Image.RenderTransform>
<TranslateTransform x:Name="imgTranslateTransform" />
</Image.RenderTransform>
</Image>
QBindableBase 最简单的基础类,继承INotifyPropertyChanged接口
QBindableAppBase 应用基础类,提供了应用开发需要的常用组件
DapperAppBindableBase 支持Dapper数据库访问的应用基础类
EfAppBindableBase 支持Entity Framework数据库访问的应用基础类
QValidatableBase 可校验的基础类,为输入提供校验支持
QEditBindableBase 可编辑基础类,提供了增删改接口
DapperEditBindableBase 受Dapper数据库支持的编辑基础类
EfEditBindableBase Entity Framework支持的编辑基础类
**ViewModel基类的选择原则考虑以下几点:**是否带输入绑定?是否需要数据校验?是否增删改对象?是否支持数据库?
(1) 不需要输入绑定的ViewModel,应该从QBindableAppBase继承
(2) 不需要输入和编辑,但需要访问数据库的ViewModel,应该从DapperAppBindableBase 或EfAppBindableBase继承
(3) 需要数据校验,但不需要访问数据库,比如自己存文件或云存储的,应该从QValidatableBase 继承
(4) 需要数据校验,作为增删改操作对象,又不想用Dapper和Ef的,从QEditBindableBase继承
(5) 需要数据校验,绑定编辑输入,同时又要访问数据库的,从DapperEditBindableBase或EfEditBindableBase继承
(6) 只要求通知属性触发,追求极简效率,不用任何其他组件的,从最基础的QBindableBase 继承
QBindableAppBase作为ViewModel的核心基类,提供了大量组件,使得ViewModel中可以直接使用,大大提升开发效率,下面分别介绍:
public IFormat Format { get; } //为Xaml绑定提供格式化和转换服务
protected IServiceProvider ServiceProvider { get; } //IOC容器的Provider服务
protected ILocalization Localization { get; } //多语言服务
protected IMessageBox MsgBox { get; } //消息框服务
protected IMapper Mapper { get; } //对象映射服务
protected ILogger Logger { get; } //日志服务
protected ILocalStorage Ls { get; } //LocalStorage服务
protected IMessenger Messenger { get; } //消息服务
protected IQConfiguration Configuration { get; } //配置服务
//触发器,用于Xaml中绑定触发
public QBindableAppBase Triggers(params object[] propertyNames);
//获取LocalStorage
protected virtual TValue GetLs<TValue>([CallerMemberName] string propertyName = null);
//获取LocalStorage或者返回默认值
protected virtual TValue GetLsOrDefault<TValue>(TValue defaultValue, [CallerMemberName] string propertyName = null);
//设置LocalStorage
protected virtual void SetLs<TValue>(TValue value, [CallerMemberName] string propertyName = null);
LocalStorage机制提高了开发的便利性,以json为格式,可见,可编辑,因此有必要特别予以介绍。详见LocalStorageDemo,下面举一个简单例子:
public class MainWindowViewModel : QBindableAppBase
{
public string Name
{
get => GetLs<string>(); //自动从LocalStorage中获取,如果不存在返回null
set => SetLs(value); //自动保存到LocalStorage
}
}
<TextBox Text="{Binding Path=Name}" />
以上代码看似和普通代码没有什么区别,但你会神奇的发现,Name的值被持久化了,也就是说界面修改的Name自动存盘,下次程序启动时,Name自动加载为上次的值。这里有开发人员可能会有疑问,如果高频率修改Name的值,是不是每次都要写硬盘,存在效率问题?这个问题框架也有所考虑,如果2秒内没有新的更改,才会写入硬盘,否则只是修改内存,不存在效率问题。
WPF应用中,有些设置参数和选项,需要写入配置文件持久化。实现配置有各种各样的方式,Quick框架提供了一种便利方式
//只需要定义一个类,标记QConfigurationSection,并设置一个不重复的名称,框架将自动维护它的读取和存储
[QConfigurationSection("app")]
public class AppConfig
{
public string UserName { get; set; }
public string Password { get; set; }
}
/* 配置默认存储于程序安装目录下的appsettings.json里了,如果要设置初始值,可以在工程目录下的
appsettings.json里设置它,并且将文件生成动作设置为:如果较新则复制 */
{
"app": {
"UserName": "liming",
"Password" : "123456"
}
}
//使用方法,上面的AppConfig类已经被全局自动注入了,因此可以在任何地方使用它
public class LoginViewModel : QBindableAppBase
{
public void DoLogin()
{
AppConfig config = ServiceProvider.GetService<AppConfig>();
}
}
需要注意的是,如果程序崩溃,没有正常退出,配置将丢失,如果需要立即将配置存盘,需要注入IQConfiguration服务, 调用其Save方法
public class LoginViewModel : QBindableAppBase
{
public void DoLogin()
{
AppConfig config = ServiceProvider.GetService<AppConfig>();
config.Name = "new user";
config.Password = "password";
Configuration.Save();
}
}
针对开发中的一些痛点:比如应用单一实例启动后,再启动第二个时,如何激活第一个。Quick框架进行了高效实现(不用socket,不用管道,不用窗口消息),减轻了开发人员负担。
public partial class App : QWpfApplication
{
/// <summary>
/// 以下属性也可以直接在Xaml里设置,这里是为了添加注释
/// </summary>
public App()
{
//应用程序单例,当设置该项时,在启动第二个实例时,将自动激活已启动的,阻止第二个启动,
//需要注意的是,不同应用应该使用不同的Id
SingleInstanceAppId = "C620B9C4-92FF-4980-B536-6C7A52135761";
//设置该项后,在应用退出时,将采用结束进程的方式执行快速关闭,加速程序关闭的速度,默认为false
QuickShutdown = true;
//设置该项后,当程序出现异常时,将阻止程序崩溃,并记录异常日志,默认为true
PreventShutdownWhenCrashed = true;
}
//当设置了SingleInstanceAppId,启动第二个实例时,第一个实例会调用以下方法,可以在这里写自定义代码
protected override void OnStartupNextInstance()
{
}
}
消息机制作为提效解耦的利器,也是必不可少的,各框架都提供了自己的消息中间件。Quick框架中,消息机制的使用也非常简单。
//首先定义一条消息
public class SearchFinishedMessage
{
public SearchFinishedMessage(string keyword)
{
Keyword = keyword;
}
public string Keyword { get; }
}
//订阅消息
public partial class MainWindow : Window
{
private readonly MainWindowViewModel _mainVM;
public MainWindow(MainWindowViewModel mainVM)
{
InitializeComponent();
_mainVM = mainVM;
this.DataContext = _mainVM;
Messenger.Default.Register<SearchFinishedMessage>(this, msg =>
{
tbxKeyword.Focus();
tbxKeyword.SelectAll();
});
}
}
//发送消息
[SingletonDependency]
public class MainWindowViewModel : QBindableBase
{
public string Keyword { get; set; }
public void DoSearch()
{
if (Keyword.IsNullOrEmpty())
{
return;
}
//调用消息服务,发送消息
Messenger.Default.Send(new SearchFinishedMessage(Keyword));
}
}
消息框作为常用的功能,开发人员用起来也很繁琐,常常要写很多代码,有几个痛点:
(1) 参数多,尤其是OkCancel这种类型的,要指定一堆参数
(2) 系统消息框,不支持多语言
(3) 弹出的消息内容如果要支持多语言,也很麻烦
(4)纠结能否在ViewModel中弹消息框
Quick框架解决了以上4个问题
public class MainWindowViewModel : QValidatableBase
{
public void BatchDelete()
{
//直接提供QuestionOKCancel和QuestionYesNo方法来解决参数多的问题
if (MsgBox.QuestionOKCancel("确定要执行批量删除吗?") == MessageBoxResult.Cancel)
{
return;
}
//其中MsgBox.Question.BatchDelete为定义在多语言文件中的字符串资源的Key
//注:当以@开头时,将从资源中搜索,以便支持多语言
if (MsgBox.QuestionOKCancel("@MsgBox.Question.BatchDelete") == MessageBoxResult.Cancel)
{
return;
}
}
}
自动UI机制是本框架的一大特色,这里简单予以介绍,详细情况请参考AutoUIDemo。
//枚举定义
public enum OccupationType
{
Student,
Doctor,
Teacher
}
//XAML代码如下
<QEditPanel>
<QElement Name="Occupation" Width="100" />
</QEditPanel>
//ViewModel代码
public class EditPanelDemoViewModel : QValidatableBase
{
[QEnumComboBox]
public OccupationType Occupation { get; set; }
}
通过在ViewModel中为枚举标记QEnumComboBox特性,Xaml中的QElement将运行时自动渲染出ComboBox效果,并自动绑定,这个机制又节省了大量代码
Quick框架本身提供一套可选的UI皮肤样式,包含黑白两种风格,开发者如果不满意有两个选择,一个是直接用Quick皮肤的源码进行重写,实现完全自定义的皮肤;相比于其他第三方框架的复杂代码,Quick的皮肤源码要简单的多。第二个选择是使用第三方的皮肤,比如Material Design或Handy Control等UI。
Quick框架皮肤集成(参见QuickThemeDemo工程),在app.xaml里添加如下代码
黑色风格:
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/Quick.Wpf;component/Themes/AutoUI.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Quick.Wpf.Themes;component/Themes/ColorBlack.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Quick.Wpf.Themes;component/Themes/ThemeConcise.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
白色风格:
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/Quick.Wpf;component/Themes/AutoUI.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Quick.Wpf.Themes;component/Themes/ColorBlue.xaml"/>
<ResourceDictionary Source="pack://application:,,,/Quick.Wpf.Themes;component/Themes/ThemeConcise.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
这里需要说明的是引用AutoUI.xaml是为了使用Quick框架的自动UI和一些扩展控件,不需要使用的情况下可以不引入。
另外,Quick框架提供了一个轻量的样式呈现默认界面,在不使用任何皮肤的情况下,建议引入该样式文件,获得更好的界面呈现
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/Quick.Wpf;component/Themes/Lite.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
<Application.Resources>
以Material Design为例,使用方法跟不用Quick框架时没有区别,如下所示:
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/Quick.Wpf;component/Themes/AutoUI.xaml"/>
<materialDesign:BundledTheme BaseTheme="Light" PrimaryColor="DeepPurple" SecondaryColor="Lime" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Defaults.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
Quick框架提供了一个强大的布局面板SpaceStackPanel,兼具Grid的部分功能和StackPanel的功能,可以很容易创建可伸缩,适应分辨率的布局,减少大量布局代码,可以说“一板走天下“,详细例子可参见LayoutDemo工程。
微信布局的例子:
<Window x:Class="LayoutDemo.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:LayoutDemo"
xmlns:qk="http://quick.cvbox.cn/2020"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
mc:Ignorable="d" WindowStartupLocation="CenterScreen"
Title="QuickFramework - 布局演示,模拟微信布局" Height="720" Width="900">
<Window.Resources>
<x:Array Type="sys:String" x:Key="dataSource">
<sys:String>zhangsan</sys:String>
<sys:String>lisi</sys:String>
<sys:String>wangwu</sys:String>
</x:Array>
</Window.Resources>
<SpaceStackPanel Orientation="Horizontal">
<!--左-->
<SpaceStackPanel Background="#2e2e2e" Width="50">
<!--上-->
<SpaceStackPanel Width="30" Margin="0,12,0,0" Spacing="12">
<Button Height="30" />
<Button Height="30" />
<Button Height="30" />
<Button Height="30" />
<Button Height="30" />
</SpaceStackPanel>
<!--中,无内容,设置站位-->
<SimplePanel SpaceStackPanel.Weight="*" />
<!--下-->
<SpaceStackPanel Width="30" Margin="0,0,0,14">
<Button Height="30" />
<Button Height="30" />
<Button Height="30" />
</SpaceStackPanel>
</SpaceStackPanel>
<!--中-->
<SpaceStackPanel Width="220" Margin="0,14">
<!--搜索框-->
<SpaceStackPanel Orientation="Horizontal" Height="28">
<TextBox SpaceStackPanel.Weight="*" />
<Button Content="+" Width="32" />
</SpaceStackPanel>
<!--会话列表,占满-->
<SpaceStackPanel SpaceStackPanel.Weight="*">
<ItemsControl ItemsSource="{StaticResource dataSource}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<!--有跨行跨列需求的局部布局使用Grid-->
<Grid Height="46" Margin="0,0,0,14">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="46" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="auto" />
</Grid.ColumnDefinitions>
<Rectangle Fill="Gray" Grid.RowSpan="2" />
<TextBlock Text="文件传输助手" VerticalAlignment="Center" Grid.Column="1" Margin="10,0,0,0" />
<TextBlock Text="[图片]" Grid.Row="1" VerticalAlignment="Center" Grid.Column="1" Margin="10,0,0,0" Foreground="Gray" />
<TextBlock Text="10:12" Grid.Column="2" VerticalAlignment="Center" Margin="10,0,0,0" Foreground="Gray" />
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</SpaceStackPanel>
</SpaceStackPanel>
<!--右,可伸缩的部分,设置SpaceStackPanel.Weight="*",因为有可调区域,用Grid才能使用GridSplitter-->
<Grid SpaceStackPanel.Weight="*" Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="*" MinHeight="100" />
<RowDefinition Height="8" />
<RowDefinition Height="300" MinHeight="140" />
</Grid.RowDefinitions>
<!--标题-->
<TextBlock Text="文件传输助手" FontSize="18" Margin="0,0,0,10" />
<!--消息显示-->
<TextBox Grid.Row="1">
</TextBox>
<!--分隔条-->
<GridSplitter Grid.Row="2" Background="Transparent" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" />
<!--输入区域-->
<SpaceStackPanel Grid.Row="3" Spacing="8" >
<!--工具区-->
<SpaceStackPanel Orientation="Horizontal" Height="30">
<!--工具区左-->
<SpaceStackPanel Orientation="Horizontal">
<Button Width="30" />
<Button Width="30" />
<Button Width="30" />
<Button Width="30" />
</SpaceStackPanel>
<!--占位-->
<SimplePanel SpaceStackPanel.Weight="*" />
<!--工具区右-->
<SpaceStackPanel Orientation="Horizontal">
<Button Width="30" />
<Button Width="30" />
</SpaceStackPanel>
</SpaceStackPanel>
<!--输入区,占满-->
<TextBox SpaceStackPanel.Weight="*" />
<!--发送按钮-->
<Button Content="发送(S)" Height="30" HorizontalAlignment="Right" Width="80"/>
</SpaceStackPanel>
</Grid>
</SpaceStackPanel>
</Window>