wenmo8 发布的文章

DevExpress提供的树形列表控件TreeList和树形下拉列表控件TreeListLookupEdit都是非常强大的一个控件,它和我们传统Winform的TreeView控件使用上有所不同,我一般在Winform开发中根据情况混合使用这些控件,不过整体来看,基于DevExpress的TreeList和TreeListLookupEdit表现相对比较好看一些,本篇随笔主要介绍这两个控件在实际Winform项目中的使用处理。

1、DevExpress的TreeList控件使用

例如在菜单管理中,我们知道菜单一般情况下是层次节点的,我们为了直观显示的需要,一般把菜单用树列表控件进行展示,其中就会用到我们说的TreeList控件,如下界面所示。

其中TreeList和一个输入SearchControl来一起协同使用,可以提高界面的友好性,我们可以通过输入关键字进行节点的过滤显示。

如输入过滤内容后查询过滤树列表节点,如下所示,这样可以给用户快速模糊检索指定的树节点。

有了大概的感官认识,我们来了解下TreeList控件的使用情况

在菜单界面的设计视图下,我们添加一个ContextMenuStrip的右键菜单控件,然后编辑一些菜单项目,如下界面所示。

然后拖入一个图片集合的ImageCollection控件,并为它增加一些控件图片(也可以保留为空,然后动态指定,如本例一样)

初始化树列表,我们一般分为几个函数,一个是初始化树列表,一个是绑定查询过滤的处理,一个是把数据绑定到树列表上去,如下代码所示。

        private async void FrmMenu_Load(objectsender, EventArgs e)
{
//列表信息 InitTree();
InitSearchControl();
awaitBindTree();
}

上面使用了异步操作,我们一般也可以不用异步,这里根据情况处理吧。

初始化树列表的操作代码如下所示。

        /// <summary>
        ///初始化树控件/// </summary>
        private voidInitTree()
{
this.tree.Columns.Clear();//添加显示列 this.tree.Columns.Add(new TreeListColumn { FieldName = "Id", Caption = "Id" });//增加一个隐藏的字段,存储需要的ID this.tree.Columns.Add(new TreeListColumn { FieldName = "Name", Caption = "菜单名称", Width = 160, VisibleIndex = 0});//设置树控件的层次关系及属性 tree.KeyFieldName = "Id";
tree.ParentFieldName
= "PID";this.tree.OptionsBehavior.Editable = false;this.tree.OptionsView.EnableAppearanceOddRow = true;this.tree.OptionsView.EnableAppearanceEvenRow = true;this.tree.OptionsDragAndDrop.DragNodesMode = DragNodesMode.None;//不允许拖动//设置树的图标集合及逐级图标 this.tree.SelectImageList = this.imageCollection1;this.tree.CustomDrawNodeImages += (object sender, CustomDrawNodeImagesEventArgs e) =>{//int maxCount = this.imageCollection1.Images.Count;//var index = e.Node.Level < maxCount ? e.Node.Level : 0;//e.SelectImageIndex = index; var id = string.Concat(e.Node.GetValue(Id_FieldName));int index = 0;
IdIndexDict.TryGetValue(id,
outindex);
e.SelectImageIndex
=index;
};
//初始化树节点选择事件 this.tree.FocusedNodeChanged += delegate (objectsender, FocusedNodeChangedEventArgs e)
{
this.FocusedNodeChanged();
};
}
private async voidFocusedNodeChanged()
{
if (this.tree.FocusedNode != null)
{
IsNormalSearch
= false;awaitBindData();
}
}

初始化树列表的处理代码,我们还可以使用扩展函数进一步简化它,如下所示。

       /// <summary>
        ///初始化树控件/// </summary>
        private voidInitTree()
{
this.tree.Columns.Clear();this.tree.OptionsDragAndDrop.DragNodesMode = DragNodesMode.None;//不允许拖动//控件扩展函数封装处理 this.tree.CreateColumn("Name", "菜单名称", 160, true);this.tree.InitTree("Id", "PID", null, false, false);//设置树的图标集合及逐级图标 this.tree.SelectImageList = this.imageCollection1;this.tree.CustomDrawNodeImages += (object sender, CustomDrawNodeImagesEventArgs e) =>{//int maxCount = this.imageCollection1.Images.Count;//var index = e.Node.Level < maxCount ? e.Node.Level : 0;//e.SelectImageIndex = index; var id = string.Concat(e.Node.GetValue(Id_FieldName));int index = 0;
IdIndexDict.TryGetValue(id,
outindex);
e.SelectImageIndex
=index;
};
//初始化树节点选择事件 this.tree.FocusedNodeChanged += delegate (objectsender, FocusedNodeChangedEventArgs e)
{
this.FocusedNodeChanged();
};
}

初始化查询控件SearchControl的代码如下所示。

/// <summary>
///实现树节点的过滤查询/// </summary>
private voidInitSearchControl()
{
this.searchControl1.Client = this.tree;this.tree.FilterNode += (object sender, FilterNodeEventArgs e) =>{if (tree.DataSource == null)return;string nodeText = e.Node.GetDisplayText("Name");//参数填写FieldName if (string.IsNullOrWhiteSpace(nodeText))return;bool isExist = nodeText.IndexOf(searchControl1.Text, StringComparison.OrdinalIgnoreCase) >= 0;if(isExist)
{
var node =e.Node.ParentNode;while (node != null)
{
if (!node.Visible)
{
node.Visible
= true;
node
=node.ParentNode;
}
else break;
}
}
e.Node.Visible
=isExist;
e.Handled
= true;
};
}

上面只是初始化树列表控件,我们还需要对它进行数据的绑定显示,树列表的绑定主要代码就是数据绑定和图标的绑定。

this.tree.SelectImageList = this.imageCollection1;this.tree.DataSource = result.Items;

不过我们图标由于是从数据源里面动态构建的,因此需要存在一个集合里面,方便赋值给树列表控件,如下是完整的绑定代码。

/// <summary>
///绑定树的数据源/// </summary>
private asyncTask BindTree()
{
var pageDto = newMenuPagedDto();var result = awaitMenuApiCaller.Instance.GetAll(pageDto);//用来存放对应ID和index顺序的 IdIndexDict = new Dictionary<string, int>();//重新刷新节点图片 this.imageCollection1 = newImageCollection();this.imageCollection1.ImageSize = new Size(16, 16);if (result != null && result.Items != null)
{
foreach (var menuInfo inresult.Items)
{
var image =LoadIcon(menuInfo);this.imageCollection1.Images.Add(image);//为每个菜单ID添加一个图片序号,方便查找 if(!IdIndexDict.ContainsKey(menuInfo.Id))
{
int index = this.imageCollection1.Images.Count - 1;//最后一个序号 IdIndexDict.Add(menuInfo.Id, index);
}
}
}
//imageCollection变化了,需要重新赋值给treeList this.tree.SelectImageList = this.imageCollection1;this.tree.DataSource =result.Items;this.tree.ExpandAll();
}

如果我们需要获取指定树节点的绑定的值,我们可以通过当前的FocuseNode获取字段值,如下代码所示。

        /// <summary>
        ///编辑列表数据/// </summary>
        private asyncTask EditTreeData()
{
string ID = string.Concat(this.tree.FocusedNode.GetValue(Id_FieldName));if (!string.IsNullOrEmpty(ID))
{
FrmEditMenu dlg
= newFrmEditMenu();
dlg.ID
=ID;
dlg.OnDataSaved
+= newEventHandler(dlg_OnDataSaved);
dlg.InitFunction(LoginUserInfo, FunctionDict);
//给子窗体赋值用户权限信息 if (DialogResult.OK ==dlg.ShowDialog())
{
awaitBindTree();
}
}
}

以上就是TreeList控件的使用过程,主要注意的就是数据源的绑定和图标的绑定操作即可。

如果我们还允许树列表拖动,以便进行父位置的调整,那么可以对它进行一个拖动的事件处理。

   this.tree.CalcNodeDragImageIndex += new DevExpress.XtraTreeList.CalcNodeDragImageIndexEventHandler(this.Tree_CalcNodeDragImageIndex);this.tree.DragDrop += new System.Windows.Forms.DragEventHandler(this.Tree_DragDrop);this.tree.DragOver += new System.Windows.Forms.DragEventHandler(this.Tree_DragOver);
        private void Tree_DragOver(objectsender, DragEventArgs e)
{
TreeListNode dragNode
= e.Data.GetData(typeof(TreeListNode)) asTreeListNode;
e.Effect
= GetDragDropEffect(sender asTreeList, dragNode);
}
private async void Tree_DragDrop(objectsender, DragEventArgs e)
{
TreeListNode dragNode, targetNode;
TreeList tl
= sender asTreeList;
Point p
= tl.PointToClient(newPoint(e.X, e.Y));

dragNode
= e.Data.GetData(typeof(TreeListNode)) asTreeListNode;
targetNode
=tl.CalcHitInfo(p).Node;//移动后更新数据 var id = string.Concat(dragNode.GetValue("Id")).ToInt64();var pid = string.Concat(targetNode.GetValue("Id")).ToInt64();await OrganizationUnitApiCaller.Instance.Move(new MoveOrganizationUnitDto() { Id = id, ParentId =pid });awaitBindTree();

e.Effect
=DragDropEffects.None;
}
private void Tree_CalcNodeDragImageIndex(objectsender, CalcNodeDragImageIndexEventArgs e)
{
TreeList tl
= sender asTreeList;if (GetDragDropEffect(tl, tl.FocusedNode) ==DragDropEffects.None)
e.ImageIndex
= -1; //no icon elsee.ImageIndex= 1; //the reorder icon (a curved arrow) }

2、DevExpress的TreeListLookupEdit控件使用

TreeList和TreeListLookupEdit一般都会成对出现,一般我们需要调整父节点的时候,都会涉及到这个树形的下拉列表TreeListLookupEdit控件的。

为了方便,我们一般都定义一个自定义控件来处理这些下拉列表的绑定,因此不需要每次都绑定数据以及初始化代码。

        private void FunctionControl_Load(objectsender, EventArgs e)
{
if (!this.DesignMode)
{
InitTree();
BindTree();
}
}
/// <summary> ///初始化树/// </summary> /// <returns></returns> private voidInitTree()
{
this.txtMenu.Properties.ValueMember = "Id";this.txtMenu.Properties.DisplayMember = "Name";var tree = this.treeListLookUpEdit1TreeList;
tree.Columns.Clear();
//控件扩展函数封装处理 tree.CreateColumn("Name", "菜单名称", 160, true);
tree.InitTree(
"Id", "PID", null, false, false);//设置树的图标集合及逐级图标 tree.SelectImageList = this.imageCollection1;
tree.CustomDrawNodeImages
+= (object sender, CustomDrawNodeImagesEventArgs e) =>{int maxCount = this.imageCollection1.Images.Count;var index = e.Node.Level < maxCount ? e.Node.Level : 0;
e.SelectImageIndex
=index;
};
}
/// <summary> ///绑定树列表/// </summary> /// <returns></returns> public voidBindTree()
{
var tree = this.treeListLookUpEdit1TreeList;//绑定列表数据 var result = AsyncContext.Run(() => MenuApiCaller.Instance.GetAll(newMenuPagedDto() { }));if (result != null && result.Items != null)
{
tree.DataSource
=result.Items;
tree.ExpandAll();
//展开所有 }//执行绑定后处理 if (DataBinded != null)
{
DataBinded(
null, null);
}
}

其中 AsyncContext.Run 是把异步函数当做同步使用,一般我们在用户控件上,我们都使用同步操作,避免数据绑定延迟导致没有处理

通过制定ValueMember和DisplayMember就可以正常显示内容,以及在后面存储需要的值操作。

  this.txtMenu.Properties.ValueMember = "Id";this.txtMenu.Properties.DisplayMember = "Name";

TreeListLookupEdit控件里面还是有一个TreeList控件的,这个控件的操作和我们上面说的TreeList控件操作完全一样,我们按树列表的方式初始化并绑定它即可。

界面效果如下所示。

树列表控件展开如下所示。

缓存在一个大型一点的系统里面是必然会涉及到的,合理的使用缓存能够给我们的系统带来更高的响应速度。由于数据提供服务涉及到数据库的相关操作,如果客户端的并发数量超过一定的数量,那么数据库的请求处理则以爆发式增长,如果数据库服务器无法快速处理这些并发请求,那么将会增加客户端的请求时间,严重者可能导致数据库服务或者应用服务直接瘫痪。缓存方案就是为这个而诞生,随着缓存的引入,可以把数据库的IO耗时操作,转换为内存数据的快速响应操作,或者把整个页面缓存到缓存系统里面。本篇随笔主要介绍利用ABP框架的支持实现的服务端缓存处理和Winform客户端缓存的处理。

1、缓存文章回顾

缓存的重要性不言而喻,我在博客园里面也写了很多缓存相关的文章,都是基于实际系统的总结处理。


Winform里面的缓存使用


使用ConcurrentDictionary替代Hashtable对多线程的对象缓存处理


在.NET项目中使用PostSharp,使用MemoryCache实现缓存的处理


.NET缓存框架CacheManager在混合式开发框架中的应用(1)-CacheManager的介绍和使用


在.NET项目中使用PostSharp,使用CacheManager实现多种缓存框架的处理


在Winform开发框架中下拉列表绑定字典以及使用缓存提高界面显示速度


C#开发微信门户及应用(48) - 在微信框架中整合CacheManager 缓存框架

上面这些都是和缓存相关的内容,一般来说,缓存有很多方式的实现,如MemoryCache、Redis、Memcached、Couchbase、System.Web.Caching等,为了方便我们一般使用.net的内存缓存处理,如果我们需要序列化缓存内容,那么可以采用
MemoryCache
或者Redis缓存等。后来我们通过综合考虑,基于配置方式选择不同缓存方式,在后端一般可以使用
CacheManager
的缓存处理。

如下面是基于常规架构的缓存处理分层,如果是基于Web API的服务端,那么缓存一般可以在Web API层或者它的下面一层。

如果是基于可序列化的缓存处理,它在IIS或者其他Web 容器重新启动后,缓存不会丢失,如在Redis里面,有相关的缓存记录如下所示。

2、ABP服务端缓存处理

ABP提供了缓存的抽象,它内部使用了这个缓存抽象。虽然默认的实现使用了MemoryCache,通过配置也可以使用Redis等缓存,缓存的主要接口ICacheManager。

我们可以在应用服务层的构造函数里面,注入该接口,然后使用该接口获得一个缓存对象。

官方简单的应用服务层代码如下所示。

public classTestAppService : ApplicationService
{
private readonlyICacheManager _cacheManager;publicTestAppService(ICacheManager cacheManager)
{
_cacheManager
=cacheManager;
}

实际上,我们应用服务层应该会更加复杂一些,如下是我们ABP快速开发框架的应用服务层的代码

[AbpAuthorize]public class DictDataAppService : MyAsyncServiceBase<DictData, DictDataDto, string, DictDataPagedDto, CreateDictDataDto, DictDataDto>, IDictDataAppService
{
/// <summary> ///缓存管理接口/// </summary> private readonlyICacheManager _cacheManager;private readonly IRepository<DictData, string>_repository;public DictDataAppService(IRepository<DictData, string> repository, ICacheManager cacheManager) : base(repository)
{
_repository
=repository;
_cacheManager
= cacheManager;//依赖注入缓存 }

对于字典模块,我们一般获取接口如下所示。

        /// <summary>
        ///根据字典类型ID获取所有该类型的字典列表集合(Key为名称,Value为值)/// </summary>
        /// <param name="dictTypeId">字典类型ID</param>
        /// <returns></returns>
        public async Task<Dictionary<string, string>> GetDictByTypeID(stringdictTypeId)
{
IList
<DictData> list = await Repository.GetAllListAsync(s => s.DictType_ID ==dictTypeId);

Dictionary
<string, string> dict = new Dictionary<string, string>();foreach (DictData info inlist)
{
if (!dict.ContainsKey(info.Name))
{
dict.Add(info.Name, info.Value);
}
}
returndict;
}

如果我们需要把它构建一个缓存接口,那么处理方式就是对它进行一个简单包装即可,如下代码所示。

        /// <summary>
        ///根据字典类型ID获取所有该类型的字典列表集合(使用缓存)/// </summary>
        /// <param name="dictTypeId">字典类型ID</param>
        /// <returns></returns>
        public async Task<Dictionary<string, string>> GetDictByTypeIDCached(stringdictTypeId)
{
//系统缓存默认为60分钟,可以在模块中配置具体的时间,配置后则是具体配置时间 return await _cacheManager.GetCache("DictDataAppService")
.GetAsync(dictTypeId, ()
=>GetDictByTypeID(dictTypeId));
}

默认缓存超时是60分钟,它可以改。如果你超过60分钟没有使用缓存中的项,会从缓存中自动移除。你可以配置指定的缓存或是全部的缓存。

我们可以在应用服务层模块类ApplicationModule类里面进行修改,实现对缓存的过期设置。

            //系统缓存默认为60分钟,可以在模块中配置具体的时间,配置后则是具体配置时间//所有缓存设置为2小时
            Configuration.Caching.ConfigureAll(cache =>{
cache.DefaultSlidingExpireTime
= TimeSpan.FromHours(2);
});
//特殊指定为5分钟 Configuration.Caching.Configure("DictDataAppService", cache =>{
cache.DefaultSlidingExpireTime
= TimeSpan.FromMinutes(5);
});

Redis 缓存集成

默认缓存管理使用的是内存缓存。所以,如果你有多个并发的Web服务器使用同个应用,可能会成为一个问题,在这种情况下,你需要一个分布/集中缓存服务,你就可以简单的使用Redis做为你的缓存服务器。

Redis是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sorted set --有序集合)和hash(哈希类型)。在此基础上,redis支持各种不同方式的排序。与memcached一样,为了保证效率,数据都是缓存在内存中。区别的是redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。

Redis的代码遵循ANSI-C编写,可以在所有POSIX系统(如Linux,
*
BSD, Mac OS X, Solaris等)上安装运行。而且Redis并不依赖任何非标准库,也没有编译参数必需添加。

下载地址:
https://github.com/MSOpenTech/redis/releases
下载,安装为Windows服务即可。

安装后作为Windows服务运行,安装后可以在系统的服务里面看到Redis的服务在运行了,如下图所示。

安装好Redis后,还有一个Redis伴侣Redis Desktop Manager需要安装,这样可以实时查看Redis缓存里面有哪些数据,具体地址如下:
http://redisdesktop.com/download

下载属于自己平台的版本即可

下载安装后,打开运行界面,如果我们往里面添加键值的数据,那么可以看到里面的数据了。

我们来看看如何在ABP框架中使用Redis缓存

我们现在应用服务层模块里面配置好使用Redis,如下代码所示

[DependsOn(
................
typeof(AbpRedisCacheModule) //Redis缓存加入 )]public classApplicationModule : AbpModule
{
public override voidPreInitialize()
{
............
//使用Redis缓存 int DatabaseId = -1;int.TryParse(AppSettingConfig.GetAppSetting("RedisCache", "DatabaseId"), outDatabaseId);string connectionString = AppSettingConfig.GetAppSetting("RedisCache", "ConnectionString");
Configuration.Caching.UseRedis(options
=>{
options.ConnectionString
=connectionString;
options.DatabaseId
=DatabaseId;
});
//系统缓存默认为60分钟,可以在模块中配置具体的时间,配置后则是具体配置时间//所有缓存设置为2小时//Configuration.Caching.ConfigureAll(cache =>//{//cache.DefaultSlidingExpireTime = TimeSpan.FromHours(2);//});//特殊指定为5分钟 Configuration.Caching.Configure("DictDataAppService", cache =>{
cache.DefaultSlidingExpireTime
= TimeSpan.FromMinutes(5);
});
}

Host项目配置文件,Appsetting.json配置文件如下所示,增加RedisCache的配置节点。

使用缓存处理的应用服务层接口实现如下所示

        /// <summary>
        ///根据字典类型ID获取所有该类型的字典列表集合(使用缓存)/// </summary>
        /// <param name="dictTypeId">字典类型ID</param>
        /// <returns></returns>
        public async Task<Dictionary<string, string>> GetDictByTypeIDCached(stringdictTypeId)
{
//系统缓存默认为60分钟,可以在模块中配置具体的时间,配置后则是具体配置时间 return await _cacheManager.GetCache("DictDataAppService").GetAsync(dictTypeId, () =>GetDictByTypeID(dictTypeId));
}

在测试接口页面中进行测试

查看缓存管理里面的内容,可以发现已经具有值了,如下所示。

这样我们就可以很容易的从内存缓存切换到Redis的缓存了。

实体缓存

虽然ABP缓存系统出于普通的目的,但有一个EntityCache基类,可帮你缓存实体。如果我们通过它们的Id获取的实体,我们可以用这个基类缓存它们,就不用再频繁地从数据库查询。

不过这里不对这个进行细讲了。

3、Winform客户端的缓存处理

除了在服务端进行缓存测试外,为了提高客户端的响应速度,我们还可以在Winform客户端中使用内存缓存进行缓存一些不常变化的内容的,这样可以避免频繁的请求网络接口,获取接口数据。

ABP基础模块里面也提供了一个简单的缓存类,我们可以使用它进行缓存处理。

我曾经在之前一篇随笔《
在Winform开发框架中下拉列表绑定字典以及使用缓存提高界面显示速度
》对字典模块中使用缓存进行了说明,这个我们也可以调整为ABP快速开发框架中Winform客户端的字典处理方式。

ABP中有两种cache的实现方式:MemroyCache 和 RedisCache. 如下图,两者都继承至ICache接口。ABP核心模块封装了MemroyCache 来实现ABP中的默认缓存功能。 Abp.RedisCache这个模块封装RedisCache来实现缓存。

我们可以在Winform客户端中使用AbpMemoryCache是实现内存缓存的处理。

例如我们在界面模块中使用一个字典辅助类来封装对字典模块的调用,同时可以使用缓存方式进行获取。

使用缓存处理的逻辑,如下所示

主要就是判断键值是否存在,否则就设置内存缓存即可。

然后在编写一个字典控件的扩展函数,如下所示。

        /// <summary>
        ///绑定下拉列表控件为指定的数据字典列表/// </summary>
        /// <param name="control">下拉列表控件</param>
        /// <param name="dictTypeName">数据字典类型名称</param>
        /// <param name="defaultValue">控件默认值</param>
        /// <param name="emptyFlag">是否添加空行</param>
        public static void BindDictItems(this ComboBoxEdit control, string dictTypeName, string defaultValue, bool isCache = true, bool emptyFlag = true)
{
var dict =GetDictByDictType(dictTypeName, isCache);

List
<CListItem> itemList = new List<CListItem>();foreach (string key indict.Keys)
{
itemList.Add(
newCListItem(key, dict[key]));
}

control.BindDictItems(itemList, defaultValue, emptyFlag);
}

绑定字典控件使用的时候,就非常简单了,如下代码是实际项目中对字典列表绑定的操作,字典数据在字典模块里面统一定义的。

        /// <summary>
        ///初始化数据字典/// </summary>
        private voidInitDictItem()
{
txtInDiagnosis.BindDictItems(
"入院诊断");
txtLeaveDiagnosis.BindDictItems(
"最后诊断");//初始化代码 this.txtFollowType.BindDictItems("随访方式");this.txtFollowStatus.BindDictItems("随访状态");
}

这样就非常简化了我们对字典数据源的绑定操作了,非常方便易读,下面是其中一个功能界面的下拉列表展示。

使用缓存接口,对于大量字典数据显示的界面,界面显示速度有了不错的提升。

录制视频也是一个很有意思的过程,一般需要提前准备好相关的提纲,以及整个思路的过程,在开始尝试的时候,可能还会因为有些特殊问题,而不得不重来一遍,录制视频开始用的是Snagit的软件,这个软件对视频的录制效果还是不错的,后来进一步了解并使用了OBS Studio的视频录制软件,这个软件是更专业的视频录制软件,而且它是免费的,而且提供很多控制功能,另外还可以把摄像头的视频投射在右下角(或者自己定义位置),对视频录制处理,本来想通过使用Adobe Premiere Pro 来进行一定的视频加工处理的,例如可以制作视频片头,特效等,不过这个软件是一个很强大的软件,暂时没有功夫细细研究吧。

下面是录制的一些视频,供读者了解我们开发框架的各个相关内容吧。

1、框架模块视频介绍(Winform、混合框架)

1)
公用类库介绍

2)
Winform分页控件

3)
字典管理模块介绍

4)
权限管理模块介绍

5)
附件管理模块

6)
自动升级模块

7)
人员管理模块

8)
主体框架介绍

2、Winform、混合框架开发视频

介绍Winform框架、混合框架的增量式开发的过程,各个特定主题操作介绍

1)Winform框架增量开发过程

2)Winform开发过程-客户信息表

3)Winform开发过程-产品信息表

4)混合框架增量开发过程

5)混合框架增量开发过程-产品信息

6)开发框架-底层数据库相关处理

7)开发框架-主从表界面生成

8)混合框架图片显示及存储

8)混合框架之WebAPI接入的增量开发过程

3、Bootstrap开发框架介绍

介绍Bootstrap开发框架的增量式开发的过程,各个特定主题操作介绍

1)Bootstrap开发框架介绍--基础框架

2)Bootstrap开发框架介绍--MVC框架

3)Bootstrap开发框架介绍--Bootstrap介绍

4)Bootstrap开发框架介绍—重要插件使用

4、ABP快速开发框架视频

介绍ABP快速开发框架的功能特点、框架界面,以及实现模块增量式开发的过程,各个特定主题操作介绍

1)ABP快速开发框架介绍

后续会增加相关主题,到时候再进行文章更新。

.Net Reactor 是一款比较不错的混淆工具,比VS自带的那个好用很多,一直以来也陪伴着我们的成长,虽然没有完美的混淆工具,不过也算还是不错的,至少能在一定程度上对DLL进行一定的保护处理。

不过最近客户反映我们在混合框架删除操作的时候,没有如期的实现删除操作,由于混合框架是基于Web API / WCF这样的分布式开发方式,因此和普通跟踪的方式有所不同,针对Web API的使用是比较广泛的在云端实现数据集中管理的一种方式,相对普通的调试来说,难度增加了一些,需要在服务端(本篇主要是Web API操作)进行调试,以及在客户端界面进行联合调试处理。

本篇随笔主要介绍如何在碰到基于分布式处理数据的接口的时候,对错误问题的分析和逐步缩小范围的方式进行排查,最终解决问题的分析处理过程。

1、定位问题的发生

在我们出现问题的时候,往往需要定位在那个部分出现了错误,首先我们在客户端和服务端都需要进行跟踪调试,首先我们需要在Web  API 层跟踪对应的控制器操作是否获得对应要删除记录的ID值。

我们前面功能测试的时候,发现所有删除操作都出现了无法删除的问题,因此很可能是没有传递ID值,或者转换过程中出现了问题。

我们服务器端的删除操作接口如下所示。

        /// <summary>
        ///根据指定对象的ID,从数据库中删除指定对象(用于整型主键)/// </summary>
        /// <param name="key">指定对象的ID</param>
        /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns>
[HttpPost]public virtual CommonResult Delete(KeyInfo keyInfo, string token, string signature, string timestamp, string nonce, stringappid)
{
//检查用户是否有权限,否则抛出MyDenyAccessException异常 base.CheckAuthorized(AuthorizeKey.DeleteKey, token, signature, timestamp, nonce, appid);

CommonResult result
= newCommonResult();try{if (keyInfo != null)
{
result.Success
=baseBLL.Delete(keyInfo.id);
}
}
catch(Exception ex)
{
LogTextHelper.Error(ex);
//错误记录 result.ErrorMessage =ex.Message;
}
returnresult;
}

其中的KeyInfo类是我们定义的一个实体类,定义代码如下所示。

    /// <summary>
    ///用于删除的ID对象/// </summary>
[Serializable]public classKeyInfo
{
/// <summary> ///ID主键/// </summary>public object id { get; set; }
}

我们在调试Web API控制器的时候,无法获得KeyInfo参数的值,如下界面所示。

那么可能KeyInfo无法被反序列化,又或者是KeyInfo没有传递过来,我们跟踪对应的接口,方向本来应该在客户端POST提交的ID信息,无法提交过来。

这个是针对客户端提交操作的抓包处理,本来想用Fiddler来抓取的,不过Fiddler好像无法直接抓取localhost的请求包体,通过代理设置没有处理成功,改用以前用的很顺手的 HttpAnalyzer,直接运行就可以抓取了,非常方便。

通过上面的操作,我们发现数据没有提交到控制器里面,因此排除Web API控制器反序列化对象的时候丢掉值的可能,
而是客户端根本没有提交数据过来

2、定位具体的值丢失位置

那么我们回到对删除操作的统一处理方法里面看看,有Delete和Delete2操作类似,分别对应不同类型处理。

我们发现这里的处理,是直接把ID传递过来构建一个匿名对象,然后序列化为JSON字符串提交给Web API控制器处理的。在界面上主要就是通过统一调用进行删除的,传递ID给对应接口进行处理的。

以权限系统模块,用户删除操作为例,它的界面端处理代码如下所示。

以上代码我增加了一行用来记录是否获得ID的内容的,通过日志记录,可以看到有ID传递给接口处理了。

这样看到,问题出现在接口的处理里面,也就是可能由于我对DLL采用了混淆操作,导致的匿名类解析出现了问题了。

我们首先重写一下具体类的删除接口操作,跟踪一下问题。

为了有效验证我们的问题出现在这里,我们对比勾选和取消了红色勾选,编译后的代码进行测试。

对比处理结果,我们可以看到混淆前后,接口获得的数据不同,因此可以知道是混淆导致匿名类处理出现了问题。

于是,我们对所有相关的DLL,取消对应的这个混淆选项,运行可以得到正确的结果。

以上就是我们对这个.Net Reactor混淆导致匿名类处理出现的问题处理分析,其中主要涉及到了客户端localhost地址的本地抓包处理,采用了比较方便易用的HttpAnalyzer来分析是否数据提交有问题,还是数据解析出现问题,定位问题的边界,然后逐步对界面和接口部分进行分析。

由于对DLL混淆导致的错误问题,一般来说不易推断,所以尽可能多的列出可能影响的因素,逐一测试解决,慢慢缩小范围即可获得解决问题的办法。

前阵子一直期待.net core3.0正式版本的出来,以为这个版本出来,Winform程序又迎来一次新生了,不过9.23日出来的马上下载更新VS,创建新的.net core Winform项目,发现并没有Winform窗体设计器。而微软目前则是通过插件的方式,让我们单独下载Winform设计器,这个设计器还是预览版本,很多功能还是没有实现的,只能算是一个简单的雏形,本博客案例介绍基于.net core3.0创建一个普通的WInform程序,让大家了解下基于.net core3.0创建的程序的大概模样。

1、开发环境的准备

要做基于.net core3.0的WInform开发,需要首先更新你的Visual Studio到16.3,这个版本是整合.net core3.0的,因此也是能够开发.net core Winform程序的基础。

其次是下载
winforms-designer
插件,这个是支持对Winform窗体的设计器,让我们可以通过拖动控件的方式进行界面的设计开发。

.NET Core Windows Forms 可视化设计器在将来一定是未来的Visual Studio 2019更新的一部分,但目前来说,想要可视化设计器,需要一个预发布的Visual Studio扩展。

完成这两个步骤,其他开发就和我们普通创建VS项目一样的。

创建项目后,我们可以打开对应的Winform窗体,并可以在工具箱里面看到一些Winform界面控件,好的是控件的大概和以前差不多,不好的事情是少了很多常规Winform控件,这个也是目前WInform 设计器处于开发预览版的原因所在吧。

2、创建一个WInform程序

为了创建一个简单测试的WInform程序,我们可以往里面添加一些WInform的界面控件,不过使用过程中,发现很多界面所需元素没有提供界面控件的支持,包括工具栏、属性里面都还不完善,如ImageList对象和Image对象的属性支持等,我们只能通过代码的方式进行使用。

我创建一个简单的WInform界面,拖动了一些常规的控件,但是一些控件需要使用图片的,如ListView、PictureBox等这些,需要通过代码设置(无法通过属性加入的方式指定图片)

最后界面展示效果如下所示。

窗体源码如下所示。

   public partial classForm1 : Form
{
publicForm1()
{
InitializeComponent();
}
private void button1_Click(objectsender, EventArgs e)
{
MessageBox.Show(
"你好,这是一个.net core的Winform程序", "提示信息",
MessageBoxButtons.OK, MessageBoxIcon.Information
|MessageBoxIcon.Asterisk);
}
private ImageList imageList = newImageList();private void Form1_Load(objectsender, EventArgs e)
{
var image = Image.FromFile(Path.Combine(Application.StartupPath, "SplashScreen.png"));if(image != null)
{
this.pictureBox1.Image =image;
}

imageList.Images.Clear();
var iconPath = Path.Combine(Application.StartupPath, "icons");var fileNames = Directory.GetFiles(iconPath, "*.ico");foreach(string file infileNames)
{
imageList.Images.Add(file, Image.FromFile(file));
}
this.treeView1.ImageList =imageList;foreach(TreeNode node in this.treeView1.Nodes)
{
SetNodeImage(node);
}
this.button1.Image = imageList.Images[2];
}
private voidSetNodeImage(TreeNode node)
{
foreach (TreeNode subNode innode.Nodes)
{
subNode.ImageIndex
=subNode.Level;
subNode.SelectedImageIndex
=subNode.Level;
SetNodeImage(subNode);
}
}

从中我们可以看到,.net core下的WInform程序,它的窗体元素或者相关对象,没有发生不一致命名的情况,用起来还是非常方便一致的,不过就是对应很多界面的功能,目前只能通过后台代码的方式进行补充,才能实现一个比较完整的效果,和.net Framework框架下已经完善的非常好的Winform开发,真的是差距不是一点半点,看来.net core winform开发的路还是很漫长,需要在工具层面更多的支持才行。

界面方案里面,我们看到命名空间也比以前少了很多了了。主要还是基于.net core 提供的WInform包。

我们再来看看程序目录下的文件如下所示。

由于目前我们还没有考虑第三方的.net core 层面的类库,因此这里没有使用第三方的DLL,以后整合的话,第三方相关的引用也是一个非常头大的问题,如果大多数常用的类库都有基于.net standard 的类库支持,那倒是好,否则可能会面临两难的抉择,不过.net core的Winform开发我觉得还是很值得期待的,毕竟引入一个整体的.net core开发路线,对企业或者个人来说,都是一个非常不错的开发场景。