DevExpressMVVM WPF 环境安装
- 前言
- 重要Bug(必看)
- 环境安装
- 控件目录
- Theme 主题
- LoginWindow 登陆窗口
- INavigationService 导航服务
- DockLayout Dock类型的画面布局
- TreeView 树状列表
- GridControl 数据网格控件(等于DataGrid)
- LoadingDecorator 数据加载等待遮罩层
- GridControl 分页方式
- FlowLayoutControl 流式布局(拖拽)
- GridControl.ComboBox 数据网格中添加下拉列表
- Converter 显示转换
- GridControl.Group 数据网格分组
- GridControl 滚动动画(丝滑)
- DXDialog 弹窗(重点*)
- GridControl 内控件的Command事件
- ScrollViewer 滚动条
- dxe:ImageEdit 图片修改器
前言
博主也是刚使用DevExpress所以哪里有问题和不足的地方欢迎大家提出来!
近一年博主都会做这个技术文档的更新,所以大家可以持续关注一下,如果完结的话博主会写出来!
优点:控件真是不错,各种集成,特别适合报表,工业,工具,系统等方向软件,功能强大,扩展丰富
缺点:开发文档全英文,对于英文不好的小伙伴不太友好,网络资源少(版本不一样解决方案不一样),导致开发需要摸索,建议搭配翻译软件看文档吧(官方网站有,自己去文档库下载)
重要Bug(必看)
1、在23.2和24.1都存在的一个bug,GridControl
如果做动态更新,就不要使用DevExpressMvvm自带的[GenerateProperty]
去声明数据源,而是采用ViewModel继承INotifyPropertyChanged
的方式去声明,否则无法动态加载。
2、还是上述问题,但是!可能其他控件也存在相同的问题,情景是:AViewModel.cs去更新BViewModel.cs中的一个数据源,由于DevExpressMvvm
没有依赖注入的技术支持,再加上博主没有使用Prism的事件聚合器,所以将BViewModel.cs定义为单例模式,通过A去调用B中的方法,从而改变B中的某个数据源,数据修改成功了,但是没有办法回显在画面上ItemsSourceChanged
都没有触发,最后发现通过一个事件去调用方法则可以,例如画面定义一个Button
然后绑定一个Click Command事件中就可以修改B中要改得数据源,具体原因不得而知!(由于这个问题博主已经将各个控件做成的UserControl合在一起了,等有时间在研究吧)
3、不确定是不是BUG,在GridControl中添加图片模板,模板内图片设置不上Source
<Image Source="{Binding Url}" />
改
<Image Source="{Binding Row.Url}" />
环境安装
直接官网安装https://www.evget.com/product/2346
建议使用同版本的,否则可能会有一点东西不一样,那探索起来简直是太棒了!
安装之后会有一个Demo Center 24.1 里面有大量Demo,但是进行了一部分DemoBase基类的集成,虽然不太影响阅读,但是阅读感不太好
控件目录
接下来是博主开发中涉及使用到的一些控件,有同样的小伙伴可以根据菜单直接进行查看!
Theme 主题
博主主要是要实现一种主题,不进行切换,但是根据网上资料设置好久都没成功,最后在文档中看到了预设主题的这么一个说明,上代码!
using DevExpress.Xpf.Core;
using System.Windows;
namespace UIEditor
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
static App()
{
CompatibilitySettings.UseLightweightThemes = true;
}
protected override void OnStartup(StartupEventArgs e)
{
ApplicationThemeHelper.ApplicationThemeName = LightweightTheme.VS2019Light.Name;
base.OnStartup(e);
}
}
}
主要需要重写OnStartup启动方法
LoginWindow 登陆窗口
博主做了一个登陆画面,思维一直锁在了导航里,其实对于Window也用不上导航,直接Show主界面,Close当前登录框就可以了,上代码!
LoginView.xaml
<dx:ThemedWindow
x:Class="UIEditor.Views.Login"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ViewModels="clr-namespace:UIEditor.ViewModels"
xmlns:dx="http://schemas.devexpress.com/winfx/2008/xaml/core"
xmlns:dxe="http://schemas.devexpress.com/winfx/2008/xaml/editors"
xmlns:dxmvvm="http://schemas.devexpress.com/winfx/2008/xaml/mvvm"
xmlns:dxwui="http://schemas.devexpress.com/winfx/2008/xaml/windowsui"
xmlns:dxwuin="http://schemas.devexpress.com/winfx/2008/xaml/windowsui/navigation"
Title="Login"
Width="280"
Height="320">
<dx:ThemedWindow.DataContext>
<ViewModels:LoginViewModel />
</dx:ThemedWindow.DataContext>
<Grid VerticalAlignment="Center">
<dxwui:NavigationFrame>
<dxmvvm:Interaction.Behaviors>
<dxwuin:FrameNavigationService />
</dxmvvm:Interaction.Behaviors>
</dxwui:NavigationFrame>
<StackPanel>
<TextBlock
Margin="0,0,0,40"
HorizontalAlignment="Center"
Text="UI编辑器" />
<TextBox Width="190" Margin="0,0,0,20" />
<dxe:PasswordBoxEdit
Width="190"
Margin="0,0,0,20"
VerticalAlignment="Center"
NullText="请输入密码"
NullValue="" />
<Button
Width="190"
Command="{Binding LoginCommand}"
Content="登陆" />
</StackPanel>
</Grid>
</dx:ThemedWindow>
注意dxwui
,dxwuin
是导航使用的,博主没删是等着下次用的时候直接过来粘贴大家不要管,直接看Button
中Command="{Binding LoginCommand}"
方法就可以了
LoginViewModel.cs
[GenerateCommand]
public void Login()
{
MainWindow mainWindow = new MainWindow();
mainWindow.Show();
Application.Current.MainWindow.Close();
}
大家应该注意到我只在ViewModel层声明了一个Login方法,DevExpress MVVM直接给我生成了一个LoginCommand,具体什么做法大家可以看一下底层,所以这也是DevExpress MVVM的强大之处,挺方便的
INavigationService 导航服务
由于DevExpress MVVM没提供 IoC依赖注入,博主也懒得弄了,所以在服务注入的时候需要换个方法,上代码!
XXXViewModel.cs
private INavigationService NavigationService { get { return this.GetService<INavigationService>(); } }
[GenerateCommand]
public void Login()
{
NavigationService.Navigate("MainWindow", null, this);
}
直接在ViewModel声明服务使用就可以了
XXXView.xaml
<dxwui:NavigationFrame>
<dxmvvm:Interaction.Behaviors>
<dxwuin:FrameNavigationService />
</dxmvvm:Interaction.Behaviors>
</dxwui:NavigationFrame>
需要粘一个这个用于导航就可以了(目前博主还没到真正使用导航的时候,如果有问题需要大家自己在研究一下)
DockLayout Dock类型的画面布局
博主要做一个仿照Vs2022的编译器所以会有控件布局拖拽的需求,于是看到DevExpree存在这个布局控件,于是使用了一下,实现简单,效果哇塞!上效果!
这样一个小窗口,可以按住控件的标题栏就行拖拽,与Vs2022一致,上代码!
<dx:ThemedWindow
x:Class="UIEditor.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Views="clr-namespace:UIEditor.Views"
xmlns:dx="http://schemas.devexpress.com/winfx/2008/xaml/core"
xmlns:dxa="http://schemas.devexpress.com/winfx/2008/xaml/accordion"
xmlns:dxdo="http://schemas.devexpress.com/winfx/2008/xaml/docking"
xmlns:dxdove="http://schemas.devexpress.com/winfx/2008/xaml/docking/visualelements"
xmlns:dxe="http://schemas.devexpress.com/winfx/2008/xaml/editors"
xmlns:dxmvvm="http://schemas.devexpress.com/winfx/2008/xaml/mvvm"
Title="UI编辑器"
Width="1000"
Height="800"
dx:ThemeManager.ThemeName="VS2019Light"
TitleAlignment="Center">
<Grid>
<dxdo:DockLayoutManager Margin="2">
<dxdo:LayoutGroup>
<dxdo:LayoutPanel
Caption="UI列表"
ItemWidth="*" //这个是用来做控件按百分比占宽度的
Visibility="Collapsed">
<Views:MainView />
</dxdo:LayoutPanel>
<dxdo:LayoutGroup ItemWidth="*" Orientation="Vertical"> //这个是讲DockLayout中的内容在分为几块,主要用来初始化的时候展示布局的
<dxdo:LayoutPanel AllowClose="False" Caption="层级列表">
<Views:LevelView />
</dxdo:LayoutPanel>
<dxdo:LayoutPanel Caption="资源列表" Visibility="Collapsed">
<Views:UIListView />
</dxdo:LayoutPanel>
</dxdo:LayoutGroup>
<dxdo:LayoutPanel
Caption="Untiy程序"
ItemWidth="3.5*"
Visibility="Collapsed">
<Views:UIListView />
</dxdo:LayoutPanel>
<dxdo:LayoutPanel
AllowClose="False" //禁止关闭模块了,有需要的可以删掉这个
Caption="属性区"
ItemWidth="3*">
<Views:PropertyView />
</dxdo:LayoutPanel>
</dxdo:LayoutGroup>
</dxdo:DockLayoutManager>
</Grid>
</dx:ThemedWindow>
TreeView 树状列表
博主要实现可以根据Type类型实现Icon不同,并且可修改Node节点名称的,大家先看一下代码吧!
注意引用类型的时候ImageSource
是PresentationCore
程序集的博主找了好久,一直告诉我Select返回类型不一致
XXXView.xaml
<UserControl
x:Class="UIEditor.Views.LevelView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ViewModels="clr-namespace:UIEditor.ViewModels"
xmlns:commn="clr-namespace:UIEditor.Common"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:dxdb="http://schemas.devexpress.com/winfx/2008/xaml/demobase/internal"
xmlns:dxg="http://schemas.devexpress.com/winfx/2008/xaml/grid"
xmlns:dxmvvm="http://schemas.devexpress.com/winfx/2008/xaml/mvvm"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DesignHeight="300"
d:DesignWidth="300"
mc:Ignorable="d">
<UserControl.DataContext>
<ViewModels:LevelViewModel />
</UserControl.DataContext>
<UserControl.Resources>
<commn:SolutionNodeImageSelector x:Key="_solutionNodeImageSelector" />
</UserControl.Resources>
<Grid>
<dxg:TreeViewControl
AllowEditing="True"
AllowRecursiveNodeChecking="True"
ChildNodesPath="Children"
ItemsSource="{Binding Nodes}"
NodeImageSelector="{StaticResource _solutionNodeImageSelector}" // 节点图片选择器(根据英文名读的,英文菜鸡博主)主要是这个
SearchPanelNullText="请输入名称进行检索"
ShowBorder="False"
ShowLoadingPanel="True"
ShowNodeImages="True"
ShowSearchPanel="True"
TreeViewFieldName="Name" />
</Grid>
</UserControl>
SolutionNodeImageSelector.cs
using DevExpress.Xpf.Core;
using DevExpress.Xpf.Grid;
using DevExpress.Xpf.Grid.TreeList;
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Media;
using UIEditor.Models;
using UIEditor.Models.Enum;
namespace UIEditor.Common
{
public class SolutionNodeImageSelector : TreeListNodeImageSelector
{
static SolutionNodeImageSelector()
{
ImageCache = new Dictionary<NodeType, ImageSource>();
}
static readonly Dictionary<NodeType, ImageSource> ImageCache;
public override ImageSource Select(TreeListRowData rowData)
{
LevelNode solutionNode = (rowData.Row as LevelNode);
if (solutionNode == null)
return null;
return GetImageByTypeNode(solutionNode.TypeNode);
}
public static ImageSource GetImageByTypeNode(NodeType typeNode)
{
if (ImageCache.ContainsKey(typeNode))
return ImageCache[typeNode];
var extension = new SvgImageSourceExtension() { Uri = new Uri(GetImagePathByTypeNode(typeNode)), Size = new Size(16, 16) };
var image = (ImageSource)extension.ProvideValue(null);
ImageCache.Add(typeNode, image);
return image;
}
public static string GetImagePathByTypeNode(NodeType typeNode)
{
return "pack://application:,,,/UIEditor;component/Images/SolutionExplorer/" + typeNode.ToString() + ".svg";
}
}
}
这是模板代码,没什么可以说的,官方Demo给的,直接改一下GetImagePathByTypeNode
返回的地址就好
最后是Model和Enum
Model.cs (也是官方Demo的)
using System.Collections.Generic;
using UIEditor.Models.Enum;
namespace UIEditor.Models
{
public class LevelNode {
public NodeType TypeNode { get; set; }
public string Name { get; set; }
public string TypeName { get; set; }
public string FileName { get; set; }
List<LevelNode> _children;
public IEnumerable<LevelNode> Children { get { return _children; } }
public string DisplayName { get { return string.IsNullOrEmpty(TypeName) ? Name : string.Format("{0} : {1}", Name, TypeName); } }
public string SearchString { get; set; }
public string SearchName { get; set; }
public void AddChildren(LevelNode child) {
if(_children == null)
_children = new List<LevelNode>();
_children.Add(child);
}
}
}
Enum.cs (图片要和Enum同名,要不就在模板里写一下切换吧)
/// <summary>
/// 层级树状类型字典
/// </summary>
public enum NodeType
{
Image,
Text,
Color
}
注意:官方Demo有个坑,也是博主没看清,博主发现官方给的Demo初始化的时候,所有的节点都默认展开,最后发现例子里有这么一段
Loaded="{DXEvent Handler='@Self.ExpandNode(0);@Self.ExpandNode(1)'}"
ExpandNode
这个坑货,直接展开节点
AutoExpandAllNodes 自动展开所有节点 True or False
GridControl 数据网格控件(等于DataGrid)
这个目前没什么需要注意的,可能就是实现跟DataGrid不太一样,博主是要在一个Cell添加多个图片,贴出来大家自己看一下吧,以这个为范例大家自由发挥
XXXView.xaml
<dxg:GridControl
Grid.Row="1"
ItemsSource="{Binding Properties, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
ShowBorder="False">
<dxg:GridColumn
Width="80"
FieldName="ActionCode"
Header="动作编码" />
<dxg:GridColumn
Width="80"
FieldName="Total"
Header="最大数量"
IsSmart="True" />
<dxg:GridColumn
Width="80"
FieldName="Quantity"
Header="实际数量"
IsSmart="True" />
<dxg:GridColumn
Width="*"
CellTemplate="{StaticResource _ImageTemplate}"
Header="图片" />
<dxg:GridControl.View>
<dxg:TableView AllowPaging="True" />
</dxg:GridControl.View>
</dxg:GridControl>
_ImageTemplate(直接定义在xaml开头也行,定义一个模板文件也行)
<UserControl.Resources>
<DataTemplate x:Key="_ImageTemplate">
<StackPanel Orientation="Horizontal">
<ItemsControl ItemsSource="{Binding Row.Images}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<Image Source="{Binding Url}" />
<TextBlock Text="{Binding Size}" />
<TextBlock Text="{Binding Name}" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</StackPanel>
</DataTemplate>
</UserControl.Resources>
ViewModel没什么东西无非就是定义一个数据源,弄两个实体,太多了就不贴出来了,有问题可以留言
注意:注意如果需要修改请使用FieldName的形式,如果用Binding绑定的话 无法用控件本身的修改
去掉标题栏上面的Seach按钮ShowGroupPanel="False"
,去掉左侧指示器ShowIndicator="False"
上个效果图:
LoadingDecorator 数据加载等待遮罩层
用于画面加载数据的等待效果,需要进行线程处理,防止UI线程阻塞,话不多说,上代码!
<dx:LoadingDecorator IsSplashScreenShown="{Binding LoadingFlag}">
<dx:LoadingDecorator.SplashScreenTemplate> // 如果不需要自定义东西 这层直接删掉,默认文字是Loading
<DataTemplate>
<dx:WaitIndicator Content="正在加载文件目录..." DeferedVisibility="True" />
</DataTemplate>
</dx:LoadingDecorator.SplashScreenTemplate>
中间放处理逻辑
</dx:LoadingDecorator>
LoadingFlag
就是一个bool值
如果不需要特殊效果SplashScreenTemplate
这层就可以直接删掉,如果需要特殊处理,可以根据博主给出的代码,去文档里看,有官方Demo
在贴一个防止线程阻塞的方法
LoadingFlag = true;
var files = await Task.Run(() =>
{
return this.GetPngImage(folderPath); // 这里面是耗时的逻辑处理
});
Properties = new ObservableCollection<PropertyModel>(files);
GridControl 分页方式
话不多说直接上代码!
<dxg:GridControl>
<dxg:GridControl.View>
<dxg:TableView AllowPaging="True" />
</dxg:GridControl.View>
</dxg:GridControl>
主要是AllowPaging
这个属性就是是否开启分页
至于扩展自己去官方上看吧,链接: Devexpress WPF控件文档中心>>数据分页
FlowLayoutControl 流式布局(拖拽)
这个布局可以像StackPanel指定内部布局方式,主要是可以实现布局内控件"拖拽排序"功能,话不多说,上代码!
View.xaml
<DataTemplate x:Key="_ImageTemplate">
<dxlc:FlowLayoutControl
AllowItemMoving="True"
AnimateItemMoving="True"
Background="White"
ItemsSource="{Binding Row.Images}"
Orientation="Horizontal"
ShowLayerSeparators="True">
<dxlc:FlowLayoutControl.ItemTemplate>
<DataTemplate>
<StackPanel Width="75" VerticalAlignment="Bottom">
<Image Source="{Binding Url}" />
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Bottom"
Text="{Binding Size}" />
<TextBlock VerticalAlignment="Bottom" Text="{Binding Name}" />
</StackPanel>
</DataTemplate>
</dxlc:FlowLayoutControl.ItemTemplate>
</dxlc:FlowLayoutControl>
</DataTemplate>
这个没什么好说的,StackPanel层就是内容,可以自行修改,StackPanel可以不要,其他的扩展属性,请去文档或者官方查看
GridControl.ComboBox 数据网格中添加下拉列表
在GridControl中添加一个ComboBox 可以使用ComboBoxEditSettings
,具体的上代码!
博主一开始进入一个误区,就是如何知道选择的是哪行的ComboBox值那,后来发现是套在GridColumn中的,只要GridColumn中的FieIdName和ComboBoxEditSettings的ValueMember是同类型值就可以联动修改…一开始人傻了找好久…
View.xaml
xmlns:dxe="http://schemas.devexpress.com/winfx/2008/xaml/editors"
xmlns:dxg="http://schemas.devexpress.com/winfx/2008/xaml/grid"
<dxg:GridColumn
Width="120"
FieldName="ActionCode"
Header="动作类型">
<dxg:GridColumn.EditSettings>
<dxe:ComboBoxEditSettings
DisplayMember="Name" // 指定显示字段
IsTextEditable="False"
ItemsSource="{Binding ActionTypes}"
ValueMember="ActionCode" /> // 指定选中后的返回值
</dxg:GridColumn.EditSettings>
</dxg:GridColumn>
ViewModel就不粘出来了,无非就是定义一个ActionTypes集合
Converter 显示转换
这个大家用的应该很多了,但是DevExpress提供了一个简便写法
View.xaml
xmlns:dxmvvm="http://schemas.devexpress.com/winfx/2008/xaml/mvvm"
<UserControl.Resources>
<dxmvvm:ObjectToObjectConverter x:Key="ActionCodeTypeConverter">
<dxmvvm:MapItem Source="0" Target="待机,技能自身" />
<dxmvvm:MapItem Source="1" Target="行走,技能过程" />
<dxmvvm:MapItem Source="2" Target="跑步,技能目标" />
</dxmvvm:ObjectToObjectConverter>
</UserControl.Resources>
<dxg:GridColumn
Width="80"
Binding="{Binding ActionCode, Converter={StaticResource ActionCodeTypeConverter}}"
GroupIndex="0"
Header="动作" />
这没什么好说的,很好理解!
GridControl.Group 数据网格分组
会有需要根据某个字段进行分组的情况,博主在网上查了好久,没有什么特别明确的内容,直到看到一位网友的资料,其实很简单,只需要在需要的Column
上加一个GroupIndex
就可以了
附上这位网友的文章: wpf devexpress 排序、分组、过滤数据
附上官方说明文档:ColumnBase.SortOrder,Sort Data,Grouping
<dxg:GridColumn
Width="80"
Binding="{Binding ActionCode, Converter={StaticResource ActionCodeTypeConverter}}"
GroupIndex="0" // 加上这个就可以了,如果有多个的话Index值根据优先级排就行
Header="动作" />
上个效果图:
GridControl 滚动动画(丝滑)
在GridControl.View中的TableView中添加AllowScrollAnimation=“True”
dxg:GridControl.View>
<dxg:TableView AllowEditing="True" AllowScrollAnimation="True" />
</dxg:GridControl.View>
DXDialog 弹窗(重点*)
使用场景:父画面调用ShowDialog调用Window弹窗,弹窗关闭时在将数据回传到父画面
MainViewModel.cs
var viewModel = new AddPropertyPopViewModel() { GroupConfigCount = int.Parse(GroupConfigCount.Trim()) }; // 子画面ViewModel
var view = new AddPropertyPopView() { DataContext = viewModel }; // 子画面View
var dialog = new DXDialog
{
WindowStartupLocation = System.Windows.WindowStartupLocation.CenterScreen,
Title = "新增动作",
Content = view,
Width = 800,
Height = 800
};
var result = dialog.ShowDialog();
if (result == true)
{
// 这里是自己的处理逻辑
if (viewModel.ActionTypeItem != null)
{
foreach (var item in viewModel.Properties)
{
item.ActionCode = viewModel.ActionTypeItem.ActionCode;
}
}
Properties.AddRange(viewModel.Properties);
}
官方的例子不好用,首先是IDialogService没有初始化,即使用GetService也不行,解决方法:
在使用IDialogService的xaml中添加这段,其他Service同理
<dxmvvvm:Interaction.Behaviors>
<dx:DialogService DialogWindowStartupLocation="CenterOwner">
<dx:DialogService.ViewTemplate>
<DataTemplate>
<Views:AddPropertyPopView />
</DataTemplate>
</dx:DialogService.ViewTemplate>
</dx:DialogService>
</dxmvvvm:Interaction.Behaviors>
博主尝试使用了订阅事件的方法传值,不知道DevExpressMvvm底层做什么了,viewModel对象永远不是活动的ViewModel,所以获取不到更新的值
GridControl 内控件的Command事件
博主知道是添加RelativeSource={RelativeSource AncestorType={x:Type UserControl}
来指向UserControl获取Command事件
但是就是不好用。
解决方法:
Command="{Binding Path=DataContext.RemovPropertyCommand, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
没错 加个Path=DataContext.
ScrollViewer 滚动条
<ScrollViewer VerticalScrollBarVisibility="Auto">
</ScrollViewer>
没什么说的,把要带滚动条的控件放里面就可以了
dxe:ImageEdit 图片修改器
此控件支持图片预览,图片修改,图片保存等等功能
<dxe:ImageEdit
x:Name="ImageEdit"
Width="120"
Height="120"
HorizontalAlignment="Left"
ShowMenu="False" // 控制悬浮图片中是否弹出菜单
Source="{Binding Url}" />
这个没什么好说的,可以看一下官方Demo有效果,但是需要注意EditValue是一个Bitmap类型,由于博主是需要用地址进行展示与修改,所以不太适用