在我们对数据进行重要修改调整的时候,往往需要跟踪记录好用户操作日志。一般来说,如对重要表记录的插入、修改、删除都需要记录下来,由于用户操作日志会带来一定的额外消耗,因此我们通过配置的方式来决定记录那些业务数据的重要调整。本篇随笔介绍如何在基于SqlSugar的开发框架中,实现对用户操作日志记录的配置设置,以及根据配置信息自动实现用户操作日志记录。

1、用户操作日志记录的配置处理

前面提到,由于用户操作日志会带来一定的额外消耗,因此我们通过配置的方式来决定记录那些业务数据的重要调整。

首先我们在系统中定义一个用户操作日志记录表和一个操作日志配置信息表,系统根据配置进行记录重要的修改调整信息。

列表展示信息如下所示

有了这些信息记录,我们可以在操作基类函数中,通过判断SqlSugar实体类信息中的是否插入、更新、删除的重要设置,可以决定记录它们那些操作日志信息。

下面列表记录了对一些表的增加、修改、删除、以及一些重要的系统操作日志信息,如“密码重置”、“密码修改”、“用户过期设置”等操作日志。

2、在基类中实现用户操作日志记录处理

上面界面展示了如何通过配置,自动记录用户对某业务的相关重要操作记录的界面。系统之所以能够进行相关的信息记录,是在基类函数中定义了相关的逻辑,根据配置逻辑,把插入对象的详细信息、修改对象的变化比记录、删除对象的详细信息进行写入,以及对一些重要的处理,如重置密码等,进行自定义的信息记录的。

下面我们来看看如何在基类中处理这些操作。

例如,我们在删除记录的时候,有时候接收的是实体类的ID,有时候接收的是实体类,那么对于这些条件,我们相应的进行日志处理,如下代码所示。

        /// <summary>
        ///删除指定ID的对象/// </summary>
        /// <param name="id">记录ID</param>
        /// <returns></returns>
        public virtual async Task<bool>DeleteAsync(TEntity input)
{
await OnOperationLog(input, OperationLogTypeEnum.删除);return awaitEntityDb.DeleteAsync(input);
}
/// <summary> ///删除指定ID的对象/// </summary> /// <param name="id">记录ID</param> /// <returns></returns> public virtual async Task<bool>DeleteAsync(TKey id)
{
await OnOperationLog(id, OperationLogTypeEnum.删除);return awaitEntityDb.DeleteByIdAsync(id);
}

其中我们根据日志的操作,定义一个枚举的对象,如下所示。

    /// <summary>
    ///操作日志的枚举类型/// </summary>
    public enumOperationLogTypeEnum
{
增加,
删除,
修改
}

对于删除记录的Id,我们需要把它转换为对应的实体类,然后进行记录的。

        /// <summary>
        ///统一处理实体类的日志记录/// </summary>
        /// <param name="id">实体对象Id</param>
        /// <param name="logType">记录类型</param>
        /// <returns></returns>
        protected override asyncTask OnOperationLog(TKey id, OperationLogTypeEnum logType)
{
var enableLog = awaitCheckOperationLogEnable(logType);if(enableLog)
{
var input = await this.EntityDb.GetByIdAsync(id);if (input != null)
{
string note =JsonConvert.SerializeObject(input, Formatting.Indented);awaitAddOperationLog(logType.ToString(), note);
}
}
await Task.CompletedTask;//结束处理 }

其中
CheckOperationLogEnable
就是用来判断是否存在指定操作类型的配置信息的,如果存在,那么就记录操作日志。

我们是根据实体类的全名进行判断,如果存在指定的操作设置,就返回True,如下所示。(刚好基类中可以判断泛型约束TEntity的全名)

        /// <summary>
        ///判断指定的类型(增加、删除、修改)是否配置启用/// </summary>
        /// <param name="logType">指定的类型(增加、删除、修改)</param>
        /// <returns></returns>
        protected async Task<bool>CheckOperationLogEnable(OperationLogTypeEnum logType)
{
var result = false;string tableName = typeof(TEntity).FullName;//表名称或者实体类全名 var settingInfo = await this._logService.GetOperationLogSetting(tableName);if (settingInfo != null)
{
if (logType ==OperationLogTypeEnum.修改)
{
result
= settingInfo.UpdateLog > 0;
}
else if (logType ==OperationLogTypeEnum.增加)
{
result
= settingInfo.InsertLog > 0;
}
else if (logType ==OperationLogTypeEnum.删除)
{
result
= settingInfo.DeleteLog > 0;
}
}
returnresult;
}

对于插入记录,我们也可以同时进行判断并处理日志信息。

        /// <summary>
        ///创建对象/// </summary>
        /// <param name="input">实体对象</param>
        /// <returns></returns>
        public virtual async Task<bool>InsertAsync(TEntity input)
{
SetIdForGuids(input);
//如果Id为空,设置有序的GUID值 await OnOperationLog(input, OperationLogTypeEnum.增加);//判断并记录日志 return awaitEntityDb.InsertAsync(input);
}

对于更新原有记录,它也只需要接收更新前的对象,然后进行判断处理即可。

        /// <summary>
        ///更新对象/// </summary>
        /// <param name="input">实体对象</param>
        /// <returns></returns>
        public virtual async Task<bool>UpdateAsync(TEntity input)
{
SetIdForGuids(input);
//如果Id为空,设置有序的GUID值 await OnOperationLog(input, OperationLogTypeEnum.修改);//判断并记录日志 return awaitEntityDb.UpdateAsync(input);
}

比较两者,我们需要提供一个操作日志方法重载用于记录信息即可。

由于修改的信息,我们需要对比两个不同记录之间的差异信息,这样我们才能友好的判断那些信息变化了。也就是更新前后两个实体对象之间的属性差异信息,需要获取出来。

        /// <summary>
        ///统一处理实体类的日志记录/// </summary>
        /// <param name="input">实体对象</param>
        /// <param name="logType">记录类型</param>
        /// <returns></returns>
        protected override asyncTask OnOperationLog(TEntity input, OperationLogTypeEnum logType)
{
var enableLog = awaitCheckOperationLogEnable(logType);if (enableLog && input != null)
{
if (logType ==OperationLogTypeEnum.修改)
{
var oldInput = await this.EntityDb.GetByIdAsync(input.Id);//对于更新记录,需要判断更新前后两个对象的差异信息 var changeNote = oldInput.GetChangedNote(input); //计算差异的部分 if (!string.IsNullOrEmpty(changeNote))
{
awaitAddOperationLog(logType.ToString(), changeNote);
}
}
else{//对于插入、删除的操作,只需要记录对象的信息 var note =JsonConvert.SerializeObject(input, Formatting.Indented);awaitAddOperationLog(logType.ToString(), note);
}
}
await Task.CompletedTask;//结束处理 }

而对于差异信息,我能定义一个扩展函数来处理他们的差异信息,如下所示。

    /// <summary>
    ///对象属性的处理操作/// </summary>
    public static classObjectExtensions
{
/// <summary> ///对比两个属性的差异信息/// </summary> /// <typeparam name="T">对象类型</typeparam> /// <param name="val1">对象实例1</param> /// <param name="val2">对象实例2</param> /// <returns></returns> public static List<Variance> DetailedCompare<T>(thisT val1, T val2)
{
var propertyInfo =val1.GetType().GetProperties();return propertyInfo.Select(f => newVariance
{
Property
=f.Name,
ValueA
= (f.GetValue(val1, null)?.ToString()) ?? "", //确保不为null ValueB = (f.GetValue(val2, null)?.ToString()) ?? ""})
.Where(v
=> !v.ValueA.Equals(v.ValueB)) //调用内置的Equals判断 .ToList();
}
/// <summary> ///把两个对象的差异信息转换为JSON格式/// </summary> /// <typeparam name="T">对象类型</typeparam> /// <param name="val1">对象实例1</param> /// <param name="val2">对象实例2</param> /// <returns></returns> public static string GetChangedNote<T>(thisT oldVal, T newVal)
{
var specialList = new List<string> { "edittime", "createtime", "lastupdated"};var list = DetailedCompare<T>(oldVal, newVal);var newList = list.Select(s => new { Property = s.Property, OldValue = s.ValueA, NewValue =s.ValueB })
.Where(s
=> !specialList.Contains(s.Property.ToLower())).ToList();//排除某些属性 string note = null;if (newList?.Count > 0)
{
//增加一个ID属性记录显示 var id = EntityHelper.GetEntityId(oldVal)?.ToString();
newList.Add(
new { Property = "Id", OldValue = id, NewValue =id });

note
=JsonConvert.SerializeObject(newList, Formatting.Indented);
}
returnnote;
}
public classVariance
{
public string Property { get; set; }public string ValueA { get; set; }public string ValueB { get; set; }
}
}

这样我们通过LINQ把两个对象的差异信息生成,就可以用来记录变更操作的信息了,最终可以获得类似下面界面提示的差异信息。

也就是获得类似字符串的差异信息。

[
{
"Property": "PID","OldValue": "-1","NewValue": "0"},
{
"Property": "OfficePhone","OldValue": "","NewValue": "18620292076"},
{
"Property": "WorkAddr","OldValue": "广州市白云区同和路**小区**号","NewValue": "广州市白云区同和路330号君立公寓B栋1803房"},
{
"Property": "Id","OldValue": "1","NewValue": "1"}
]

最后的属性Id,是我们强行加到变化列表中的,因为不记录Id的话,不清楚那个记录变更了。

这样我们就实现了增删改的重要操作的记录,并且由于是基类实现,我们只需要在系统中配置决定哪些业务类需要记录即可自动实现重要日志的记录。

另外,我们在类别中还发现了其他一些不同类别的重要操作日志,如重置密码、修改密码、用户过期设置等,这些操作我们提供接口给这些处理调用即可。

        /// <summary>
        ///设置用户的过期与否/// </summary>
        /// <param name="userId">用户ID</param>
        /// <param name="expired">是否禁用,true为禁用,否则为启用</param>
        public async Task<bool> SetExpire(int userId, boolexpired)
{
bool result = false;var info = await this.GetAsync(userId);if (info != null)
{
info.IsExpire
=expired;
result
= await this.UpdateAsync(info);if(result)
{
//记录用户修改密码日志 string note = string.Format("{0} {1}了用户【{2}】的账号", this.CurrentApiUser.FullName, expired ? "禁用" : "启用", info.Name);await base.AddOperationLog("用户过期设置", note);
}
}
returnresult;
}

其中
AddOperationLog
就是我们调用基类插入指定类型和日志信息的记录的,通过自定义类型和自定义日志信息,可以让我们弹性化的处理一些重要日志记录。

系列文章:

系列文章:


基于SqlSugar的开发框架的循序渐进介绍(1)--框架基础类的设计和使用


基于SqlSugar的开发框架循序渐进介绍(2)-- 基于中间表的查询处理


基于SqlSugar的开发框架循序渐进介绍(3)-- 实现代码生成工具Database2Sharp的整合开发


基于SqlSugar的开发框架循序渐进介绍(4)-- 在数据访问基类中对GUID主键进行自动赋值处理


基于SqlSugar的开发框架循序渐进介绍(5)-- 在服务层使用接口注入方式实现IOC控制反转


基于SqlSugar的开发框架循序渐进介绍(6)-- 在基类接口中注入用户身份信息接口


基于SqlSugar的开发框架循序渐进介绍(7)-- 在文件上传模块中采用选项模式【Options】处理常规上传和FTP文件上传


基于SqlSugar的开发框架循序渐进介绍(8)-- 在基类函数封装实现用户操作日志记录


基于SqlSugar的开发框架循序渐进介绍(9)-- 结合Winform控件实现字段的权限控制


基于SqlSugar的开发框架循序渐进介绍(10)-- 利用axios组件的封装,实现对后端API数据的访问和基类的统一封装处理


基于SqlSugar的开发框架循序渐进介绍(11)-- 使用TypeScript和Vue3的Setup语法糖编写页面和组件的总结


基于SqlSugar的开发框架循序渐进介绍(12)-- 拆分页面模块内容为组件,实现分而治之的处理


基于SqlSugar的开发框架循序渐进介绍(13)-- 基于ElementPlus的上传组件进行封装,便于项目使用


基于SqlSugar的开发框架循序渐进介绍(14)-- 基于Vue3+TypeScript的全局对象的注入和使用


基于SqlSugar的开发框架循序渐进介绍(15)-- 整合代码生成工具进行前端界面的生成


基于SqlSugar的开发框架循序渐进介绍(16)-- 工作流模块的功能介绍


基于SqlSugar的开发框架循序渐进介绍(17)-- 基于CSRedis实现缓存的处理


基于SqlSugar的开发框架循序渐进介绍(18)-- 基于代码生成工具Database2Sharp,快速生成Vue3+TypeScript的前端界面和Winform端界面

在Winform开发中有时候我们为了不影响主UI线程的处理,以前我们使用后台线程BackgroundWorker来处理一些任务操作,不过随着异步处理提供的便利性,我们可以使用Async-Awati异步任务处理替换原来的后台线程BackgroundWorker处理方式,更加的简洁明了。

在一些耗时的操作过程中,在长时间运行时可能会导致用户界面 (UI) 处于停止响应状态,因此使用使用Async-Awati异步任务处理或者后台线程BackgroundWorker来处理一些任务操作很有必要。

在使用BackgroundWorker的过程中,我们可以定义自己的状态参数信息,从而实现线程状态的实时跟踪以及进度和信息提示,方便我们及时通知UI进行更新。

现在使用Async-Awati异步任务处理,一样可以在处理过程中通知UI更新进度和提示信息。

1、回顾BackgroundWorker后台线程的处理代码

我们先来了解一下BackgroundWorker后台线程的操作代码,对比下再介绍使用Async-Awati异步任务处理和通知操作。

一般的使用代码是需要初始化后台线程对象的,如下代码所示。

    public partial classMainFrame : BaseForm
{
/// <summary> ///增加一个变量来记录线程状态/// </summary> private bool IsThreadRunning = false;private BackgroundWorker worker = newBackgroundWorker();publicMainFrame()
{
InitializeComponent();

Portal.gc.InitData();

worker.WorkerSupportsCancellation
= true; //支持取消 worker.WorkerReportsProgress = true; //支持报告进度 worker.DoWork += worker_DoWork; //处理过程 worker.RunWorkerCompleted += worker_RunWorkerCompleted; //完成操作 worker.ProgressChanged += worker_ProgressChanged; //报告进度 }

例如进度条的通知,主要就是计算总任务的数量,并用于显示当前的任务进度信息,实例代码如下所示

        /// <summary>
        ///进度条的通知/// </summary>
        void worker_ProgressChanged(objectsender, ProgressChangedEventArgs e)
{
this.barProgress.EditValue =e.ProgressPercentage;
CollectStateInfo stateInfo
= e.UserState asCollectStateInfo;if (stateInfo != null)
{
var message = string.Format("正在采集 {0} 的 {1} , 项目名称为:{2}", stateInfo.TotalRecords, stateInfo.CompletedRecord + 1, stateInfo.CurrentItemName);this.lblTips.Text =message;this.barTips.Caption =message;//记录运行位置 JobParameterHelper.SaveData(newCurrentJobParameter(stateInfo));
}
}

后台进程处理的关键事件就是处理过程的代码实现,它处理任务的时候,把当前的状态通过事件方式通知UI显示。

        private void backgroundWorker1_DoWork(objectsender, DoWorkEventArgs e)
{
BackgroundWorker worker
= sender asBackgroundWorker;
Random r
= newRandom();int numCount = 0;while (worker.CancellationPending == false)
{
int num = r.Next(0, 10000);if (num % 5 == 0)
{
numCount
++;
worker.ReportProgress(
0, num);
Thread.Sleep(
1000);
}
}
e.Result
=numCount;
}

触发任务开始的时候,我们调用代码如下所示。

    if (!worker.IsBusy)
{
worker.RunWorkerAsync(stateInfo);
}

任务完成后,通知更新界面即可。

void worker_RunWorkerCompleted(objectsender, RunWorkerCompletedEventArgs e)
{
//还原按钮状态 InitCollectState();
IsThreadRunning
= false;string message = "采集操作完成";
MessageDxUtil.ShowTips(message);
}

2、使用Async-Awati异步任务处理代替BackgroundWorker

为了测试使用Asyn-Await异步处理,我创建一个简单的Demo程序,用于测试其效果。

窗体里面放置一个按钮,触发按钮执行任务操作,并逐步提示进度条信息,完成后提示任务完成。

为了在异步处理中提示进度信息,我们引入了Progress 线程通知对象。

定义一个线程通知的Progress对象,如下所示。这里的int也可以换为自定义的对象类,以方便承载更多的信息。

  var reporter = new Progress<int>(progressChanged);

其中progressChanged 是我们定义的一个通知UI显示进度的处理函数,如下所示。

        /// <summary>
        ///报告进度/// </summary>
        /// <param name="percentage">当前进度</param>
        void progressChanged(intpercentage)
{
this.progressBar1.EditValue =percentage;this.progressPanel.Caption = percentage == 100 ? "任务已完成": "任务正在处理";this.progressPanel.Description = String.Format("完成【{0}%】", percentage);
}

接着我们定义一个处理任务的WorkStart方法,接收一个Progress对象,如下代码所示。

      var reporter = new Progress<int>(progressChanged);var result = await this.WorkStart(reporter);

为了简单样式异步调用,我们这里只是延迟了一下处理任务,实际处理的话,调用异步方法即可。

        /// <summary>
        ///执行任务/// </summary>
        private async Task<CommonResult> WorkStart(IProgress<int>progress)
{
var result = newCommonResult();for(int i = 0; i < 100; i++)
{
await Task.Delay(100);
progress.Report(i
+ 1);
}
result.Success
= true;returnresult;
}

我们可以看到,任务每次执行到一个节点,就会调用对象方法Report进行通知处理。

而任务完成后,我们简单的通知处理即可。整段代码如下所示。

    /// <summary>
    ///Async Await异步线程处理/// </summary>
    public partial classFrmAsyncAwaitDemo : DevExpress.XtraEditors.XtraForm
{
publicFrmAsyncAwaitDemo()
{
InitializeComponent();
this.progressBar1.Visible = false;this.progressPanel.Visible = false;
}
private async void btnStart_Click(objectsender, EventArgs e)
{
this.btnStart.Enabled = false;this.progressBar1.Visible = true;this.progressPanel.Visible = true;var reporter = new Progress<int>(progressChanged);var result = await this.WorkStart(reporter);this.WorkCompleted(result);
}
/// <summary> ///任务完成/// </summary> /// <param name="result">返回结果CommonResult</param> voidWorkCompleted(CommonResult result)
{
if(result.Success)
{
//操作成功的处理 }var alert = newAlertControl();
alert.FormLocation
=AlertFormLocation.TopRight;
alert.AutoFormDelay
= 2000;
alert.Show(
this, "任务提示", result.Success ? "任务处理完成,操作成功": result.ErrorMessage);this.progressBar1.Visible = false;this.progressPanel.Visible = false;this.btnStart.Enabled = true;
}
/// <summary> ///报告进度/// </summary> /// <param name="percentage">当前进度</param> void progressChanged(intpercentage)
{
this.progressBar1.EditValue =percentage;this.progressPanel.Caption = percentage == 100 ? "任务已完成": "任务正在处理";this.progressPanel.Description = String.Format("完成【{0}%】", percentage);
}
/// <summary> ///执行任务/// </summary> private async Task<CommonResult> WorkStart(IProgress<int>progress)
{
var result = newCommonResult();for(int i = 0; i < 100; i++)
{
await Task.Delay(100);
progress.Report(i
+ 1);
}
result.Success
= true;returnresult;
}
}

在我们实际的案例中,文件上传处理就使用了这种方式来通知UI线程,任务处理的代码如下所示。

因此使用Async-Awati异步任务处理代替BackgroundWorker,代码更加简便,而且使用
IProgress
接口类来处理通知,也是非常方便的。

在进行项目开发的时候,刚好需要用到对字符串表达式进行求值的处理场景,因此寻找了几个符合要求的第三方组件LambdaParser、DynamicExpresso、Z.Expressions,它们各自功能有所不同,不过基本上都能满足要求。它们都可以根据相关的参数进行字符串表达式的求值,本篇随笔介绍它们三者的使用代码,以及总结其中的一些经验。

数学表达式求值应该是最常见的,一般我们在应用程序中如果需要计算,是需要对参数进行类型转换,然后在后台进行相应计算的。但是如果是计算一些符合的式子或者公式,特别是参数不一定的情况下,这个就比较麻烦。利用第三方组件,对表达式进行快速求值,可以满足我们很多实际项目上的需求,而且处理起来也很方便。

这几个第三方组件,它们的GitHub或官网地址:

https://github.com/nreco/lambdaparser

https://github.com/dynamicexpresso/DynamicExpresso

https://eval-expression.net/eval-execute

不过Z.Expressions是收费的,前两者都是免费的。

我使用字符串表达式进行求值的场景,主要就是想对一个SQL条件的表达式,转换为普通的字符串表达式,然后根据对象的参数值,进行求值处理,这几个表达式求值组件都支持这样的操作,为了更好演示它们的使用效果及代码,我们专门创建了一个案例代码进行测试验证,确认满足我的实际需求。

1、Z.Expressions.Eval 表达式解析

Z.Expression.Eval是一个免费开源的(后续收费了),可扩展的,超轻量级的公式化语言解析执行工具包,可以在运行时解析C#表达式的开源免费组件。Z.Expressions从2.0开始支持了NetCore,但是收费的。参考地址:
https://riptutorial.com/eval-expression/learn/100000/getting-started
或者
https://eval-expression.net/eval-execute

在运行时解析C#表达式,例如一些工资或者成本核算系统,就需要在后台动态配置计算表达式,从而进行计算求值。

下面对几个不同的案例代码进行介绍及输出结果验证

匿名类型处理

//匿名类型
string expression = "a*2 + b*3 - 3";int result = Eval.Execute<int>(expression, new { a = 10, b = 5});
Console.WriteLine(
"{0} = {1}", expression, result); //a*2 + b*3 - 3 = 32

指定参数

//指定参数
expression = "{0}*2 + {1}*3 - 3";
result
= Eval.Execute<int>(expression, 10, 5);
Console.WriteLine(
"{0} = {1}", expression, result);//{0}*2 + {1}*3 - 3 = 32

类对象

//类对象
expression = "a*2 + b*3 - 3";dynamic expandoObject = newExpandoObject();
expandoObject.a
= 10;
expandoObject.b
= 5;

result
= Eval.Execute<int>(expression, expandoObject);
Console.WriteLine(
"{0} = {1}", expression, result); //a*2 + b*3 - 3 = 32

字典对象

//字典对象
expression = "a*2 + b*3 - 3";var values = new Dictionary<string, object>()
{
{
"a", 10},
{
"b", 5}
};

result
= Eval.Execute<int>(expression, values);
Console.WriteLine(
"{0} = {1}", expression, result);//a*2 + b*3 - 3 = 32

委托类型

//委托类型1
expression = "{0}*2 + {1}*3";var compiled = Eval.Compile<Func<int, int, int>>(expression);
result
= compiled(10, 15);
Console.WriteLine(
"{0} = {1}", expression, result);//{0}*2 + {1}*3 = 65//委托类型2 expression = "a*2 + b*3";
compiled
= Eval.Compile<Func<int, int, int>>(expression, "a", "b");
result
= compiled(10, 15);
Console.WriteLine(
"{0} = {1}", expression, result);//a*2 + b*3 = 65

字符串扩展支持

//字符串扩展支持-匿名类型
expression = "a*2 + b*3 - 3";
result
= expression.Execute<int>(new { a = 10, b = 5});
Console.WriteLine(
"{0} = {1}", expression, result);//a*2 + b*3 - 3 = 32//字符串扩展支持-字典类型 expression = "a*2 + b*3 - 3";
values
= new Dictionary<string, object>()
{
{
"a", 10},
{
"b", 5}
};
result
= expression.Execute<int>(values);
Console.WriteLine(
"{0} = {1}", expression, result);//a*2 + b*3 - 3 = 32

可以看出,该组件提供了非常丰富的表达式运算求值处理方式。

2、NReco.LambdaParser 表达式解析

我看中这个组件的处理,主要是因为它能够传入参数是字典类型,这样我可以非常方便的传入各种类型的参数,并且这个组件比较接近SQL语法,可以设置利用常规的=代替表达式的==,这样对于SQL语句来说是方便的。

它的案例代码如下所示。

/// <summary>
///NReco.LambdaParser 表达式解析/// </summary>
private void btnLamdaParser_Click(objectsender, EventArgs e)
{
var lambdaParser = newNReco.Linq.LambdaParser();var dict = new Dictionary<string, object>();
dict[
"pi"] = 3.14M;
dict[
"one"] =1M;
dict[
"two"] =2M;
dict[
"test"] = "test";
Console.WriteLine(lambdaParser.Eval(
"pi>one && 0<one ? (1+8)/3+1*two : 0", dict)); //--> 5 Console.WriteLine(lambdaParser.Eval("test.ToUpper()", dict)); //--> TEST Console.WriteLine(lambdaParser.Eval("pi>one && 0<one", dict)); //--> True Console.WriteLine(lambdaParser.Eval("test.ToUpper()", dict)); //--> TEST }

同样它支持的算术符号操作有:+, -, *, /, %,以及常规的逻辑判断:==, !=, >, <, >=, <=,如果需要它允许把=作为==比较,那么设置属性 AllowSingleEqualSign  = true 即可,如下代码。

    var lambdaParser = newLambdaParser();
lambdaParser.AllowSingleEqualSign
= true;//可以使用 = 作为逻辑判断,如Title ="Leader",而不用Title =="Leader" var evalResult = lambdaParser.Eval(repalce, dict);

该组件没有过多提供例子,不过它的例子提供的关键点,基本上都能实现我们实际的表达式求值处理要求了。

3、DynamicExpresso 表达式解析

相对于LambdaParser的简洁、Z.Expressions收费处理,Dynamic Expresso 可以说是提供了一个非常强大的、免费开源的处理类库,它提供非常多的表达式求值的实现方式。

简单的字符串表达式求值如下代码

var interpreter = newInterpreter();var result = interpreter.Eval("8 / 2 + 2");

但是一般我们需要传入一定的参数进行表达式求值的。

var target = newInterpreter();double result = target.Eval<double>("Math.Pow(x, y) + 5",new Parameter("x", typeof(double), 10),new Parameter("y", typeof(double), 2));

或者

var interpreter = newInterpreter();var parameters = new[] {new Parameter("x", 23),new Parameter("y", 7)
};
Assert.AreEqual(
30, interpreter.Eval("x + y", parameters));

或者赋值指定的参数

var target = new Interpreter().SetVariable("myVar", 23);
Assert.AreEqual(
23, target.Eval("myVar"));

对于字典类型的处理,是我喜欢的方式,它的案例代码如下所示。

var interpreter = newInterpreter();var dict = new Dictionary<string, object>();
dict.Add(
"a", 1.0);
dict.Add(
"b", 2);
dict.Add(
"d", 4);
dict.Add(
"e", 5);
dict.Add(
"str", 'f');foreach (var v indict)
{
object value =v.Value;int para = 0;if (int.TryParse(v.Value.ToString(), outpara))
{
value
= (float)para;
}
interpreter.SetVariable(v.Key, value);
}
Console.WriteLine(interpreter.Eval(
"a+b").ToString()); //3 Console.WriteLine(interpreter.Eval("a/b").ToString()); //0.5 Console.WriteLine(interpreter.Eval("a > b").ToString()); //False Console.WriteLine(interpreter.Eval("str == 'f'").ToString()); //True

对于类的属性表达式查询,测试代码如下所示

    var customers = new List<Customer>{new Customer() { Name = "David", Age = 31, Gender = 'M'},new Customer() { Name = "Mary", Age = 29, Gender = 'F'},new Customer() { Name = "Jack", Age = 2, Gender = 'M'},new Customer() { Name = "Marta", Age = 1, Gender = 'F'},new Customer() { Name = "Moses", Age = 120, Gender = 'M'},
};
string whereExpression = "customer.Age > 18 && customer.Gender == 'F'";

Func
<Customer, bool> dynamicWhere = interpreter.ParseAsDelegate<Func<Customer, bool>>(whereExpression, "customer");
Console.WriteLine(customers.Where(dynamicWhere).Count());
//=> 1 var customer_query = (new List<Customer>{new Customer() { Name = "David", Age = 31, Gender = 'M'},new Customer() { Name = "Mary", Age = 29, Gender = 'F'},new Customer() { Name = "Jack", Age = 2, Gender = 'M'},new Customer() { Name = "Marta", Age = 1, Gender = 'F'},new Customer() { Name = "Moses", Age = 120, Gender = 'M'},
}).AsQueryable();
whereExpression
= "customer.Age > 18 && customer.Gender == 'F'";var expression = interpreter.ParseAsExpression<Func<Customer, bool>>(whereExpression, "customer");
Console.WriteLine(customer_query.Where(expression).Count());
//=> 1

4、SQL条件语句的正则表达式和字符串求值处理

前面介绍了几个表达式求值处理的组件,他们基本上都能够满足实际的求值处理,只是提供的功能有所侧重。

我主要希望用它来对特定的表达式进行求布尔值,判断表达式是否满足条件的。

例如对于sql条件语句:(Amount> 500 and Title ='Leader') or Age> 32, 以及一个字典对象的参数集合,我希望能够提取里面的Amount、Title、Leader、Age这样的键,然后给字典赋值,从而判断表达式的值。

由于sql表达式和C#代码的表达式逻辑语法有所差异,我们需要替换and Or 为实际的&& || 字符,因此给定替换的正则表达式:\sand|\sor

而我需要先提取条件语句的键值内容,然后获得指定的键参数,那么也要提供一个正则表达式:\w*[^>=<!'()\s] ,这个正则表达式主要就是提取特定的字符匹配。

提取内容的C#代码逻辑如下所示。

        private void btnRegexExtract_Click(objectsender, EventArgs e)
{
var source = this.txtSource.Text;//先替换部分内容 \sand|\sor source = Regex.Replace(source, this.txtReplaceRegex.Text, "");//替换表达式//增加一行记录主内容 this.txtContent.Text += "替换正则表达式后内容:";this.txtContent.AppendText(Environment.NewLine);this.txtContent.Text +=source;this.txtContent.AppendText(Environment.NewLine);//在匹配内容处理 var regex = new Regex(this.txtRegex.Text);var matches =regex.Matches(source);//遍历获得每个匹配的内容 var fieldList = new List<string>();int i = 0;foreach (Match match inmatches)
{
this.txtContent.AppendText(match.Value);this.txtContent.AppendText(Environment.NewLine);if (i++ % 2 == 0)
{
fieldList.Add(match.Value);
}
}
this.txtContent.AppendText("获得表达式键:");this.txtContent.AppendText(Environment.NewLine);this.txtContent.AppendText(fieldList.ToJson());this.txtContent.AppendText(Environment.NewLine);var repalce = ReplaceExpress(this.txtSource.Text);this.txtContent.AppendText("替换And=>&& or=>|| '=> \" 操作符后内容:");this.txtContent.AppendText(Environment.NewLine);this.txtContent.AppendText(repalce);
}
        /// <summary>
        ///替换And=>&& or=>|| '=> \" 操作符后内容/// </summary>
        /// <param name="source"></param>
        /// <returns></returns>
        private string ReplaceExpress(stringsource)
{
//操作符替换表达式 var repalce = Regex.Replace(source, @"\sand\s", "&&"); //and => && repalce = Regex.Replace(repalce, @"\sor\s", "||"); //or => || repalce = Regex.Replace(repalce, @"'", "\""); //'=> \" returnrepalce;
}

表达式处理结果如下所示

它的逻辑代码如下。

        private void btnRunExpression_Click(objectsender, EventArgs e)
{
//操作符替换表达式 var repalce = ReplaceExpress(this.txtSource.Text);this.txtContent.Text = "替换And=>&& or=>|| '=> \" 操作符后内容:";this.txtContent.AppendText(Environment.NewLine);this.txtContent.Text +=repalce;this.txtContent.AppendText(Environment.NewLine);this.txtContent.AppendText(Environment.NewLine);//(Amount> 500 and Title ='Leader') or Age> 32 var dict = new Dictionary<string, object>();
dict[
"Amount"] = 600;
dict[
"Title"] = "Leader";
dict[
"Age"] = 40;this.txtContent.AppendText("字典内容");foreach(var key indict.Keys)
{
this.txtContent.AppendText($"{key}:{dict[key]}");
}
this.txtContent.AppendText(Environment.NewLine);this.txtContent.AppendText(Environment.NewLine);//var valComparer = new ValueComparer() { NullComparison = ValueComparer.NullComparisonMode.Sql };//var lambdaParser = new LambdaParser(valComparer); var lambdaParser = newLambdaParser();
lambdaParser.AllowSingleEqualSign
= true;//可以使用=作为判断,如Title ="Leader",而不用Title =="Leader" var express1 = "(Amount> 500 && Title = \"Leader\") or Age>30";var result1 =lambdaParser.Eval(express1, dict);this.txtContent.AppendText("LambdaParser 表达式处理:");this.txtContent.AppendText(Environment.NewLine);this.txtContent.AppendText(express1 + "=>" +result1);var express2 = "( Amount> 500 && Title =\"leader\" )"; //字符串比较(''=> "") var result2 =lambdaParser.Eval(express2, dict);this.txtContent.AppendText(Environment.NewLine);this.txtContent.AppendText(express2 + "=>" +result2);var express3 = "Amount> 500";var result3 =lambdaParser.Eval(express3, dict);this.txtContent.AppendText(Environment.NewLine);this.txtContent.AppendText(express3 + "=>" +result3);var express4 = "Title = \"Leader\""; //字符串比较(''=> "") var result4 =lambdaParser.Eval(express4, dict);this.txtContent.AppendText(Environment.NewLine);this.txtContent.AppendText(express4 + "=>" +result4);this.txtContent.AppendText(Environment.NewLine);
Console.WriteLine(lambdaParser.Eval(
"Title.ToString()", dict)); //--> Leader//DynamicExpresso 表达式解析处理 this.txtContent.AppendText(Environment.NewLine);this.txtContent.AppendText(Environment.NewLine);this.txtContent.AppendText("DynamicExpresso 表达式解析处理:");var interpreter = newInterpreter();foreach (var v indict)
{
interpreter.SetVariable(v.Key, v.Value);
}
//express3 = "Amount> 500"; var result33 =interpreter.Eval(express3);this.txtContent.AppendText(Environment.NewLine);this.txtContent.AppendText(express3 + "=>" +result33);//使用''出错,字符串比较需要使用"" try{
express4
= "Title == \"Leader\"";var result44 =interpreter.Eval(express4);this.txtContent.AppendText(Environment.NewLine);this.txtContent.AppendText(express4 + "=>" +result44);
}
catch(Exception ex)
{
this.txtContent.AppendText(Environment.NewLine);this.txtContent.AppendText(express4 + ",解析出错 =>" +ex.Message);
}
//var dict = new Dictionary<string, object>();//dict["Amount"] = 600;//dict["Title"] = "Leader";//dict["Age"] = 40; this.txtContent.AppendText(Environment.NewLine);this.txtContent.AppendText(Environment.NewLine);this.txtContent.AppendText("Z.Expressions.Eval 表达式解析:");var result333 = express3.Execute<bool>(dict);this.txtContent.AppendText(Environment.NewLine);this.txtContent.AppendText(express3 + "=>" +result333);

express4
= "Title == 'Leader'"; //Z.Expressions可以接受 ' 代替 " var result444 = express4.Execute<bool>(dict);this.txtContent.AppendText(Environment.NewLine);this.txtContent.AppendText(express4 + "=>" +result444);
}

这样我们就可以转换SQL条件表达式为实际的C#表达式,并通过赋值参数,实现动态表达式的求值处理。

字段的权限控制,一般就是控制对应角色人员对某个业务对象的一些敏感字段的可访问性:包括可见、可编辑性等处理。本篇随笔结合基于SqlSugar的开发框架进行的字段控制管理介绍。

在设计字段权限的时候,我们需要了解这些还是基于RBAC的概念,基于角色进行授权的,而且我们的字段列表是属于具体的业务对象列表的,这里的业务对象是指一些我们具体的业务模块,如客户基础信息、人员基础信息、报价单等等,我们就是基于这些业务进行字段的控制的。

1、字段权限表的设计和界面管理

基于SqlSugar的开发框架,对应处理的业务对象信息的,是相关的实体对象信息,我们在数据库中设计两个数据库,一个用于存储对应实体类名称的信息,如ID,实体类全名,类路径等主体信息;一个存储对应角色配置的字段列表信息,结合起来就可以实现对应角色的字段权限控制了,数据库表设计信息如下所示。

两个业务表的部分字段信息如下所示。

系统中对于字段权限的设置界面如下所示。

通过列表对每个实体对象信息进行配置,如果没有对应的实体,从程序集中选择列出来即可加入进来。

加入对应的实体信息,我们可以根据名称反射接口获得对应字段参考的中文信息,然后进行展示,用于加入控制列表。

这样配置后,系统就记录了相关的配置信息,我们接下来就可以利用这些配置信息,在Winform界面上进行控件的显示处理操作了。

2、在Winform界面上进行控件的显示处理操作

在界面上,我们为了绑定界面控件和字段的关系,需要设置一个Tag来标识,然后根据系统配置信息,自动进行字段权限的控制处理(隐藏、设置只读,不限制等)

/// <summary>
///设置控件字段的权限显示或者隐藏(默认不使用字段权限)/// </summary>
private async voidSetPermit()
{
#region 设置控件和字段的对应关系 this.txtName.Tag = "Name";this.txtAge.Tag = "Age";#endregion //获取列表权限的列表 var permitDict = await BLLFactory<IFieldPermitService>.Instance.GetColumnsPermit(typeof(CustomerInfo).FullName, LoginUserInfo.Id.ToInt32());this.SetControlPermit(permitDict, this.layoutControl1);awaitTask.CompletedTask;
}

设置控件的字段显示方式 SetControlPermit ,代码如下所示。

        /// <summary>
        ///设置控件的可见、读写权限显示/// </summary>
        /// <param name="panel">控件对象</param>
        /// <param name="permitDict">字段和权限字典,字典值为权限控制:0可读写,1只读,2隐藏值,3不显示</param>
        /// <param name="layoutControl">如果存在布局,则使用布局控件,否则为空</param>
        public static void SetControlPermit(this Control panel, Dictionary<string, int> permitDict, LayoutControl layoutControl = null)
{
foreach (Control ctrl inpanel.Controls)
{
var baseCtrl = ctrl asBaseEdit;if (baseCtrl != null)
{
var tag = string.Concat(baseCtrl.Tag);if (!string.IsNullOrEmpty(tag) &&permitDict.ContainsKey(tag))
{
var permit =permitDict[tag];var visible = (permit == 0 || permit == 1);//2、3不可见 if (layoutControl != null)
{
var layoutItem =layoutControl.GetItemByControl(baseCtrl);if (layoutItem != null)
{
layoutItem.ToVisibility(visible);
}
}
baseCtrl.Visible
=visible;
baseCtrl.ReadOnly
= permit == 1;
}
}
ctrl.SetControlPermit(permitDict, layoutControl);
}
}

上面代码主要就是变量面板中的控件,并判断tag标签,然后进行只读、不可见、正常等的判断。

在列表界面中,我们可以通过设置隐藏字符让内容隐藏,如下界面效果所示。

如果不可见,就在界面上不显示这个列了,而非隐藏。

同样类似编辑控件界面,我们在列表界面也提供了对应的方法,用于隐藏列表字段的某些信息,如下代码所示。

//获取字段显示权限,并设置(默认不使用字段权限)
this.winGridViewPager1.gridView1.SetColumnsPermit(permitDict); 

它的实现规则也是类似,根据配置的字段权限控制点信息进行处理,决定是否显示,是否隐藏,是否正常处理。

它的permitDic也是根据配置信息读取出来进行判断即可。

//根据业务对象获取对应的显示字段,如果没有设置,那么根据FieldPermit表的配置获取字段权限列表
var permitDict = await BLLFactory<IFieldPermitService>.Instance.GetColumnsPermit(typeof(BlackIPInfo).FullName, Portal.gc.UserInfo.Id);var displayColumns = await BLLFactory<IBlackIPService>.Instance.GetDisplayColumns();if (permitDict != null && permitDict.Keys.Count > 0)
{
//0可读写,1只读,2隐藏值,3不显示 displayColumns = string.Join(",", permitDict.Keys.Where(s => permitDict[s] < 2));
}
this.winGridViewPager1.DisplayColumns = displayColumns;

下面是具体逻辑的通用控制方法,把它作为控件的扩展函数,我们就只需要一行代码调用就是写控制了

/// <summary>
///根据参数权限字典的值:0可读写,1只读,2隐藏值,3不显示,设置列的权限。/// </summary>
/// <param name="gridView">GridView对象</param>
/// <param name="fieNamePermitDict">字段和权限字典,字典值为权限控制:0可读写,1只读,2隐藏值,3不显示</param>
public static void SetColumnsPermit(this GridView gridView, Dictionary<string,int>fieNamePermitDict)
{
char passwordChar = '*';foreach (GridColumn col ingridView.Columns)
{
var include =fieNamePermitDict.ContainsKey(col.FieldName);if(include)
{
int permit =fieNamePermitDict[col.FieldName];switch(permit)
{
case 0://正常可见、可读写 col.OptionsColumn.AllowEdit = true;
col.OptionsColumn.ReadOnly
= false;
col.AppearanceHeader.ForeColor
=Color.Black;

col.Visible
= true;break;case 1://只读 col.OptionsColumn.AllowEdit = false;
col.OptionsColumn.ReadOnly
= true;
col.AppearanceHeader.ForeColor
=Color.Gray;

col.Visible
= true;break;case 2://隐藏值 var edit =col.CreateTextEdit();
col.Tag
= string.Concat(passwordChar);//用来在界面端进行判断,避免设置DisplayText edit.PasswordChar =passwordChar;
col.Visible
= true;break;case 3://不可见 col.Visible = false;break;
}
}
}
}

通过上面的代码处理,我们就能实现对Winform界面中的列表,编辑窗体的控件,进行相关的字段权限控制显示了。如下是实现的界面效果。

这样在系统后台,就可以根据需要设置一些敏感字段的信息隐藏或者只读处理了。

系列文章:


基于SqlSugar的开发框架的循序渐进介绍(1)--框架基础类的设计和使用


基于SqlSugar的开发框架循序渐进介绍(2)-- 基于中间表的查询处理


基于SqlSugar的开发框架循序渐进介绍(3)-- 实现代码生成工具Database2Sharp的整合开发


基于SqlSugar的开发框架循序渐进介绍(4)-- 在数据访问基类中对GUID主键进行自动赋值处理


基于SqlSugar的开发框架循序渐进介绍(5)-- 在服务层使用接口注入方式实现IOC控制反转


基于SqlSugar的开发框架循序渐进介绍(6)-- 在基类接口中注入用户身份信息接口


基于SqlSugar的开发框架循序渐进介绍(7)-- 在文件上传模块中采用选项模式【Options】处理常规上传和FTP文件上传


基于SqlSugar的开发框架循序渐进介绍(8)-- 在基类函数封装实现用户操作日志记录


基于SqlSugar的开发框架循序渐进介绍(9)-- 结合Winform控件实现字段的权限控制


基于SqlSugar的开发框架循序渐进介绍(10)-- 利用axios组件的封装,实现对后端API数据的访问和基类的统一封装处理


基于SqlSugar的开发框架循序渐进介绍(11)-- 使用TypeScript和Vue3的Setup语法糖编写页面和组件的总结


基于SqlSugar的开发框架循序渐进介绍(12)-- 拆分页面模块内容为组件,实现分而治之的处理


基于SqlSugar的开发框架循序渐进介绍(13)-- 基于ElementPlus的上传组件进行封装,便于项目使用


基于SqlSugar的开发框架循序渐进介绍(14)-- 基于Vue3+TypeScript的全局对象的注入和使用


基于SqlSugar的开发框架循序渐进介绍(15)-- 整合代码生成工具进行前端界面的生成


基于SqlSugar的开发框架循序渐进介绍(16)-- 工作流模块的功能介绍


基于SqlSugar的开发框架循序渐进介绍(17)-- 基于CSRedis实现缓存的处理


基于SqlSugar的开发框架循序渐进介绍(18)-- 基于代码生成工具Database2Sharp,快速生成Vue3+TypeScript的前端界面和Winform端界面

DevExpress提供了一个比较强大的图形绘制工具,可以用于绘制各种图形,如流程图、组织机构图等等,本篇随笔介绍XtraDiagram.DiagramControl的使用,以及利用代码对其属性进行控制,以及利用图形模具的自定义操作,实现一些简单流程图形的绘制和处理。

DiagramControl是类似Visio的绘图控件,以前我2006年的就接触使用Visio的二次开发,当时开始还是利用VB6 + VIsio2003进行二次开发的,后来把它改良为C# + Visio进行二次开发,DiagramControl的对象模型很类似Visio的相关对象模型,如对于工具栏的形状,称之为模具(Stencil),Visio也是称之为Stencil, DiagramControl里面的很多接口名称依旧采用Stencil进行命名,因此估计也是借鉴了很多Visio的对象设计知识,如果您对Visio二次开发感兴趣,可以参考我的随笔文章《
Visio二次开发
》,里面有很多相关的内容。

而如果想了解这个控件的相关知识和使用,参考官网的案例和说明应该是比较好的教程(
https://docs.devexpress.com/WindowsForms/118290/controls-and-libraries/diagrams/getting-started
)。

1、DiagramControl控件的使用

DiagramControl是一个界面控件,类似Visio SDK里面的DrawingControl的存在,可以通过它进行图形的绘制,各种窗口的显示和隐藏,以及跟踪各种事件的处理。

DiagramControl控件拖动到窗体中后,会自动增加一些属性窗口,上排的绘图工具中的按钮是我添加的,用来测试该控件的一些属性的控制。

1)属性窗口的显示和隐藏(折叠)

这个通过控制diagramControl1.OptionsView.PropertiesPanelVisibility 属性就可以实现对这个属性窗口的控制了。

里面显示一些系统位置和内容信息,以及一些自定义信息的窗口,后面我会介绍如何自定义处理这些模具的属性。

通过按钮处理的代码,我们可以实现对这个窗口的显示或者隐藏处理。

//切换属性窗口的显示或关闭
var status =diagramControl1.OptionsView.PropertiesPanelVisibility;
diagramControl1.OptionsView.PropertiesPanelVisibility
= (status == PropertiesPanelVisibility.Visible ? PropertiesPanelVisibility.Collapsed : PropertiesPanelVisibility.Visible);

2)模具形状窗口的显示或隐藏

模具形状的窗口,它是放在一个面板里面,我们只需要通过控制该面板的显示或者隐藏就可以了,如下代码所示。

//切换模具形状窗口的显示或关闭
var status =diagramToolboxDockPanel1.Visibility;
diagramToolboxDockPanel1.Visibility
= (status == DevExpress.XtraBars.Docking.DockVisibility.Visible ? DevExpress.XtraBars.Docking.DockVisibility.Hidden : DevExpress.XtraBars.Docking.DockVisibility.Visible);

或者通过控件的Toolbar属性进行控制,一样的效果。

//切换模具形状窗口的显示或关闭
var status = this.diagramControl1.OptionsView.ToolboxVisibility;this.diagramControl1.OptionsView.ToolboxVisibility = status == ToolboxVisibility.Closed ? ToolboxVisibility.Full : ToolboxVisibility.Closed;

3)放大缩小窗口的显示或者隐藏

同样我们也可以控制放大缩小窗口的显示或者隐藏,它也是图形绘制的一个常见的窗口。我们只需要判断或者设置diagramControl1.OptionsView.ShowPanAndZoomPanel 属性就可以了,如下代码所示。

//切换放大缩小窗口的显示或关闭
var status =diagramControl1.OptionsView.ShowPanAndZoomPanel;
diagramControl1.OptionsView.ShowPanAndZoomPanel
= !status;

4)其他属性的处理

另外,我们可以通过控制一些属性,实现对标尺、网格、只读视图等模式进行控制。

//是否显示标尺
this.diagramControl1.OptionsView.ShowRulers = this.chkRuler.Checked;//是否显示网格
this.diagramControl1.OptionsView.ShowGrid = this.chkGrid.Checked;//是否只读视图
this.diagramControl1.OptionsProtection.IsReadOnly = this.chkReadOnly.Checked;

2、绘图的处理事件

在绘制图形的时候,一般来说我们可能需要切换点选模式或者连接线模式,因此可以通过它的属性ActiveTool进行设置。在点选模式下,可以对图形进行拖动、放大缩小、旋转等处理,连接线模式下,则会加亮连接点,便于自动绘制连接线。

private void btnPointerMode_Click(objectsender, EventArgs e)
{
diagramControl1.OptionsBehavior.ActiveTool
=diagramControl1.OptionsBehavior.PointerTool;
}
private void btnConnectorMode_Click(objectsender, EventArgs e)
{
diagramControl1.OptionsBehavior.ActiveTool
=diagramControl1.OptionsBehavior.ConnectorTool;
}

当然,我们也可以通过对鼠标行为的分析来进行控制,如果鼠标悬停或者放置在图形上,就自动切换模式为连接线模式,否则为点选模式,那么只需要判断鼠标的移动行为即可自动处理,如下代码所示。

        /// <summary>
        ///实现对图形自动切换到连接点模式/// </summary>
        private void diagramControl1_MouseMove(objectsender, MouseEventArgs e)
{
if (e.Button ==MouseButtons.Left)return;

DiagramItem item
=diagramControl1.CalcHitItem(e.Location);if (item == null)
{
diagramControl1.OptionsBehavior.ActiveTool
=diagramControl1.OptionsBehavior.PointerTool;return;
}
else if (item isDiagramConnector)
{
diagramControl1.OptionsBehavior.ActiveTool
=diagramControl1.OptionsBehavior.ConnectorTool;return;
}

Rect itemBounds
= new Rect(new Point(item.Position.X, item.Position.Y), newSize(item.Width, item.Height));
PointFloat documentPoint
= diagramControl1.PointToDocument(newPointFloat(e.Location));
DiagramHitInfo[] hitInfo
=diagramControl1.CalcHitInfo(documentPoint);if (itemBounds.Contains(newPoint(documentPoint.X, documentPoint.Y)))
{
itemBounds.Inflate(
-5, -5);if (!itemBounds.Contains(newPoint(documentPoint.X, documentPoint.Y)))
{
diagramControl1.OptionsBehavior.ActiveTool
=diagramControl1.OptionsBehavior.ConnectorTool;return;
}
}
diagramControl1.OptionsBehavior.ActiveTool
=diagramControl1.OptionsBehavior.PointerTool;
}

另外图形的保存xml、PNG、PDF处理和加载代码如下所示。

/// <summary>
///保存XML和图片文件/// </summary>
private voidSaveXml()
{
var xml = Path.Combine(Application.StartupPath, "MyFlowShapes.xml");
diagramControl1.SaveDocument(xml);
var pngFile = Path.Combine(Application.StartupPath, "MyFlowShapes.png");
diagramControl1.ExportDiagram(pngFile);
}
private void btnLoadXml_Click(objectsender, EventArgs e)
{
var xml =FileDialogHelper.OpenXml();if(!string.IsNullOrEmpty(xml))
{
diagramControl1.LoadDocument(xml);
}
}

最终案例的效果如下所示。

3、注册自定义的形状

在实际的图形绘制开发中,我们可以需要创建一些指定的形状模具,那么我们弄好后一般可以存放在XML中,然后进行加载到控件上来,如下代码就是注册自定义的形状的处理。

/// <summary>
///注册自定义的形状。///自定义图形是以XML文件形式进行保存,图形需要按照规定XML格式进行绘制/// </summary>
private voidLoadShapes2()
{
var projectName = "SmallExampleDemo.Examples.XtraDiagram";using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(projectName + ".CustomContainers.xml"))
{
var stencil = DiagramStencil.Create(MyStencilId, MyStencilName, stream, shapeName =>shapeName);
DiagramToolboxRegistrator.RegisterStencil(stencil);
}
diagramControl1.SelectedStencils
= new StencilCollection(MyStencilId);//(MyStencilId, BasicShapes.StencilId); }

我们只需要设置选中的图形就可以了,其他有需要的可以从More Shapes中选择即可。

我们如果需要在属性窗口中显示自定义的属性,那么我们需要一些代码开发才能实现。

我们首先需要继承一个DiagramShape的子类,然后实现自己自定义的属性定义,如下代码所示。

对自定义属性的处理,需要在事件中实现

diagramControl1.CustomGetEditableItemProperties += DiagramControl_CustomGetEditableItemProperties;

通过对它进行判断可以实现自定义属性的显示处理

void DiagramControl_CustomGetEditableItemProperties(objectsender, DiagramCustomGetEditableItemPropertiesEventArgs e)
{
if (e.Item isDiagramShapeEx)
{
e.Properties.Add(TypeDescriptor.GetProperties(
typeof(DiagramShapeEx))["Status"]);
e.Properties.Add(TypeDescriptor.GetProperties(
typeof(DiagramShapeEx))["TypeName"]);
}
}

然后我们可以注册创建自己的模具形状集合,如下代码所示。

/// <summary>
///创建自定义的模具/// </summary>
/// <returns></returns>
DiagramStencil CreateCustomDrawShapesStencil()
{
var stencilId = "CustomedFlowShape";var stencilName = "流程图";var shapeSizeSmall = new Size(100, 37.5);var shapeSize = new Size(100, 75);

DiagramControl.ItemTypeRegistrator.Register(
typeof(DiagramShapeEx));var stencil = newDiagramStencil(stencilId, stencilName);//流程类型 stencil.RegisterTool(new FactoryItemTool("StartEnd", () => "流程开始", diagram =>{var shape = new DiagramShapeEx(BasicFlowchartShapes.StartEnd, "流程开始");
shape.Appearance.BackColor
=Color.Red;returnshape;
}, shapeSizeSmall));
stencil.RegisterTool(
new FactoryItemTool("Decision", () => "流程条件", diagram =>{var shape = new DiagramShapeEx(BasicFlowchartShapes.Decision, "流程条件");
shape.Appearance.BackColor
= Color.FromArgb(199, 115, 1);//Color.Red; returnshape;
}, shapeSize));

这两个流程开始,流程条件,我们直接是从 BasicFlowchartShapes 集合中借用过来,构建自己的自定义对象的,默认创建的对象是方形的。

如果我们需要动态构建其他自定义类型,我们可以指定它的颜色等样式,从而构建不同类型的图形。

//循环添加相关流程节点
var procNames = new List<string> { "审批", "归档", "阅办", "会签", "领导批示分阅"};//定义几个初始化颜色顺序
var colors = new List<Color>{ Color.DeepSkyBlue, Color.ForestGreen, Color.Violet, Color.Yellow, Color.Blue, Color.Orange, Color.Indigo, Color.Purple, Color.Black, Color.Brown, Color.Pink };int i = 0;foreach (string name inprocNames)
{
var shapeId = string.Format("Process_{0}", i++);

stencil.RegisterTool(
new FactoryItemTool(shapeId, () => name, diagram =>{var shape = newDiagramShapeEx(name, Status.Inactive);var index =procNames.IndexOf(name);var color = colors[index % 10];//Color.Red; var fontColor = (color == Color.Yellow) ?Color.Black : Color.White;//没什么作用//shape.ThemeStyleId = GetStyle(index);//从Accent1样式开始 DiagramShapeStyleId.Styles[index];// shape.Appearance.BackColor =color;
shape.Appearance.BorderSize
= 3;
shape.Appearance.Font
= new Font("宋体", 12f, FontStyle.Bold);
shape.Appearance.ForeColor
=fontColor;returnshape;
}, shapeSize));
}

这样就有不同颜色的图形对象了。

根据这些我们就可以绘制出自己的各种流程图了,并且也可以根据数据库的信息,进行动态绘制展示。