blazer鍏ラ棬瀹炴垬 (blazor鍏ラ棬绗旇)

上一节我们打通了数据从数据库到浏览器,接下来我们着手实现数据从浏览器到数据库。

我们仍然是从分类树开始。在Diary.Win中,我们是通过鼠标右键来实现分类树的管理的,如下图:

blazer鍏ラ棬瀹炴垬,blazor鍏ラ棬绗旇

那么在Web上要如何来实现呢?说实话,这个过程对初学者是比较难的,因为web编程没有现成的控件可用。以往,想要实现这种功能,只能通过js来实现,但是现在不一样了,我们有了blazor,几乎可以不使用js,就可以达到目的。

一、分类树的右键响应

我们想要对分类树进行管理,首先我们想到的是在树节点点右键,然后弹出一个菜单。我查了Ant Design的官方资料,给出的示例中没有相关代码,但在事件列表中我们找到了OnContextMenu这个事件:

blazer鍏ラ棬瀹炴垬,blazor鍏ラ棬绗旇

然而当我集成到项目中却发现,这个事件死活都不会触发,只要点了右键,就始终如一地弹出下面这种菜单:

blazer鍏ラ棬瀹炴垬,blazor鍏ラ棬绗旇

我翻遍了Ant Design的文档,也没有找到任何信息。

这就是我一直不愿意用第三方库的原因,因为你用了就依赖于它,一旦它某个功能不满足你的要求,你就会非常被动,因为对你来说它是个黑盒子,你完全不知道它里面是怎么运作的。不过幸运的是,我们在项目之初就考虑到了这点,它是有源码的,但是不到万不得已的时候我们不改它,因为想要改它就得去了解它、学习它,然后你才能加入你想要的代码。有源码的好处是,当我想要改它的时候,我有这个能力而不是束手无策。

于是我打开了它的源码,找到了相应的位置,做了一些改动,此处略过N个小时。然后重新编译AntDesign.dll,替换了本地的Nuget库,触发右键菜单的问题解决了。具体怎么改的就不展开了,对99%的新手来说,只要会用轮子就可以了,没必要去了解怎么修改轮子。

当然对我来说,我的兴趣不止于此。我到Ant Design的Git上提交了Issue,也算是为开源社区做一点点小贡献吧。

blazer鍏ラ棬瀹炴垬,blazor鍏ラ棬绗旇

不过尴尬的是,等Ant Design官方修复完这个Bug可能要很久,我的教程不可能等它修复再继续进行,让大家都去改Ant Design的源码又不大现实,换做是你,这时应该怎么办呢?

如果经验不足肯定会感觉很难,但当你的技术储备到了一定层次,你就可以游刃有余了,办法其实有很多。你可以继承Ant Design的Tree控件,在此基础上进行做二次封装。最初我也是这样考虑的,但是后来想到这只能解决在树控件上点右键的问题,如果在左侧边栏的空白位置点鼠标右键,那Tree控件是无法捕获的,所以我们需要自定义一个Razor组件来完成对鼠标右键的捕获。

二、自定义Razor组件

这部分的内容实际上并不适合初学者来看,正常如果控件库功能足够强大的话是不大需要对控件做二次封装的。不过既然项目需要,就索性写上吧,我尽量简单地说。大家可以随意看,看不懂的直接从Gitee上扒代码。

创建一个ContextMenuPanel.razor的组件,然后再创建一个ContextMenuPanel.razor.cs的类文件,这样它俩就可以形成一组文件了,方便管理。

blazer鍏ラ棬瀹炴垬,blazor鍏ラ棬绗旇

在ContextMenuPanel.razor.cs文件中,添加如下代码:

using System;
using System.Text.Json;
using AntDesign;
using AntDesign.JsInterop;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;

namespace Diary.Web.Blazor.WASM.Shared
{

public partial class ContextMenuPanel : AntDomComponentBase
{
[Inject]
protected IDomEventListener DomEventListener { get; set; }
protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
{
if (OnContextMenu.HasDelegate)
{
Ref = RefBack.Current;
DomEventListener.AddExclusive<JsonElement>(Ref, "contextmenu", ContextMenuHandler, true);
}
}
base.OnAfterRender(firstRender);
}

[Parameter]
public RenderFragment ChildContent { get; set; }

/// <summary>
/// Right-click tree node callback
/// </summary>
[Parameter]
public EventCallback<MouseEventArgs> OnContextMenu { get; set; }


protected override void Dispose(bool disposing)
{
DomEventListener.DisposeExclusive();
base.Dispose(disposing);
}

protected async void ContextMenuHandler(JsonElement jsonElement)
{
var eventArgs = JsonSerializer.Deserialize<MouseEventArgs>(jsonElement.ToString(),
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
await OnContextMenu.InvokeAsync(eventArgs);
}

}
}

我们大概过一遍代码,我尽量挑重点地简单说:

上面一堆的using就是引用类库,过。

public partial class ContextMenuPanel : AntDomComponentBase

partical忘了之前有没有讲过了,这个是声明分部类,有了这个声明,一个类的声明就可以写在多个文件了。

AntDomComponentBase是Ant Design的一个基类,Blazor的底层实际上还是用到Javascript,我们可以通过IJSRuntime来与网页元素进行交互,AntDomComponentBase就是对IJSRuntime的封装,当然也可以自己写,这里我就直接用它的了,比较省事。

[Inject]是注入的意思,这玩意我还没有完全理解跟创建一个实例全部的区别都有哪些。大概看了下资料,初步理解是inject的方式相当于是一个单体,省去了你再自己为类声明单体的麻烦。IDomEventListener就是监听控件事件的接口。

protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
{
if (OnContextMenu.HasDelegate)
{
Ref = RefBack.Current;
DomEventListener.AddExclusive<JsonElement>(Ref, "contextmenu", ContextMenuHandler, true);
}
}
base.OnAfterRender(firstRender);
}

OnAfterRender是一个虚函数,重载后就可以自行处理了。这段代码的意思是,页面第一次渲染后,如果声明了OnContextMenu代理,就把这个控件的contextmenu的事件转到ContextMenuHandler上,true的意思是去掉默认的行为,这里指的就是屏蔽浏览器的右键菜单。

[Parameter]
public RenderFragment ChildContent { get; set; }

[Parameter]表示ChildContent是此Razor组件的一个参数,ChildContent就是Razor组件的子内容,这个等会再讲。

[Parameter]
public EventCallback<MouseEventArgs> OnContextMenu { get; set; }

OnContextMenu是回调函数的声明,我们可以把我们自己的函数赋值给它,等到事件到来时,就会调用到我们自己的函数,这个过程被称为代理。

Dispose就是释放资源。

protected async void ContextMenuHandler(JsonElement jsonElement)
{
var eventArgs = JsonSerializer.Deserialize<MouseEventArgs>(jsonElement.ToString(),
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
await OnContextMenu.InvokeAsync(eventArgs);
}

是自定义的右键菜单处理函数,鼠标按键信息都以json格式存储在了jsonElement信息中。InvokeAsync字面意思就是异步调用。这段代码的意思就是把鼠标信息作为参数调用我们指定的回调函数。

在ContextMenuPanel.razor文件中,添加如下代码:

@inherits AntDomComponentBase
<div @ref="Ref" style="width:100%; min-height:700px; height:100%; background-color:aquamarine;">
@if (ChildContent != null)
{
@ChildContent
}
</div>

@code {

}

@inherits 就是razor表达继承类的意思;

<div @ref="Ref" style="width:100%; min-height:700px; height:100%; background-color:aquamarine;">
@if (ChildContent != null)
{
@ChildContent
}
</div>

这个div就是ContextMenuPanel这个自定义组件的内容了;

@ref=Ref,就是这个div对应变量Ref,这个声明在了AntDomComponentBase中;

style就是定义的显示风格,这个可以自己随意定,我这里主要是为了演示方便;

具体调用代码,在Index.razor文件中:

<ContextMenuPanel OnContextMenu="OnContextMenu">
<Tree @ref="menuTree" TItem="DiaryTreeNode<Model.Category>" DataSource="@_trees" TitleExpression="x => x.DataItem.Entity.Name" ChildrenExpression="x => x.DataItem.Nodes" ShowLine="true">
</Tree>
</ContextMenuPanel>
void OnContextMenu(MouseEventArgs args)
{
}

OnContextMenu就是回调函数,ChildContent中的内容就是<Tree>...</Tree>控件的内容。

以上代码,就是ContextMenuPanel这个自定义Razor组件的全部内容,我们来看下效果:

blazer鍏ラ棬瀹炴垬,blazor鍏ラ棬绗旇

当我们按下鼠标右键的时候,断点就被激活了。即便是在Tree节点上点右键,仍然可以触发,满足我们的要求。

说实话这些代码放在入门阶段确实有点难为同学们了,主要的难点我觉得还是函数代理这块比较难理解。有C/C++编程基础的,如果理解函数指针应该就比较容易理解。

关于代理的概念,我打个比方大家可能会比较容易理解。假设你是公司老板,手下有一批员工,他们各司其职,每天他们都忙些啥你不知道。你想把最核心的销售业务管理起来,于是你指定了员工A,告诉大家,所有订单相关的事情都得经由A处理,这个员工A就成为了你的代理。

结合上面的例子:一批员工就是这些控件,ContextMenuPanel就是销售业务,contextmenu指的就是销售订单,void OnContextMenu(MouseEventArgs args)就是员工A。原本你啥事都不管,但是现在你委托了员工A去干某件事,这件事就在你的掌控中了。当然如果你觉得员工A做得不好,你可以也换成员工B,只需要换个人,整套流程是不需要变的。

以上内容,大家能理解多少就理解多少吧,看不懂就直接扒代码,如果大家有兴趣了解可以留言,我以后可以专门出几节文章来讲这个事情。

三、弹出右键菜单

有了事件就好办了,Web上弹出菜单,基本上都是事先把窗体做好,然后控制是否显示以及显示位置。

在ContextMenuPanel中我们增加Menu组件,完整代码如下:

<ContextMenuPanel OnContextMenu="OnContextMenuFunction">
<Tree @ref="menuTree" TItem="DiaryTreeNode<Model.Category>" DataSource="@_trees" TitleExpression="x => x.DataItem.Entity.Name" ChildrenExpression="x => x.DataItem.Nodes" ShowLine="true">
</Tree>
<Menu Style="@menuStyle">
<MenuItem OnClick="CreateNode">
@menu_create_text
</MenuItem>
</Menu>
</ContextMenuPanel>

@code
{
//其他代码
string menuStyle = "display:none;";
void CreateNode(MouseEventArgs args)
{
menuStyle = "display:none;";
}

string menu_create_text = "创建子节点";
   Tree<DiaryTreeNode<Model.Category>> menuTree = null;
   void OnContextMenuFunction(MouseEventArgs args)
   {
if (menuTree.SelectedNode == null)
{
menu_create_text = "创建根节点";
       }
       else
       {
           menu_create_text = "创建子节点";
       }
       menuStyle = string.Format("position:fixed; left:{0}px; top:{1}px; width:200px; z-index:999", args.ClientX, args.ClientY);
   }
}

ContextMenuPanel 中我增加了 Menu 组件。大家注意我用的是 Style =" @menuStyle"的形式,menuStyle对应的就是类中的一个字符变量,这样我就可以通过控制menuStyle的值来动态控制Menu的显示风格了。

position:fixed; left:{0}px; top:{1}px; width:200px; z-index:999

这段css的意思:固定位置显示,左上坐标为鼠标位置,宽度200像素,Z序999。Z序就是窗口的叠放顺序,999基本意味着在最顶层了,谁也覆盖不了它,如果有就再加999。

所以逻辑就是默认隐藏,当点击右键后,就显示在鼠标位置。

同理,菜单项menu_create_text也是一个动态值,我不选中树节点时它的文字应该是创建根节点,选中树节点时,它的文字应该是创建子节点。

我们来看下演示:

blazer鍏ラ棬瀹炴垬,blazor鍏ラ棬绗旇

到了这里,你是不是能够隐隐地感觉到了Blazor的强大了。真的可以不用去关心Dom对象和Javascript了。你只需要关心哪些内容是动态的,哪些内容是静态的,然后就是用C#来处理业务逻辑。

由于篇幅原因,本节内容暂时进行到这里,我们下节继续。

----------------------------------------------------

本教程项目源码已作为开源项目加入到Gitee,代码内容会随教程实时更新,大家有兴趣的话可以关注我,以获得最及时的更新。

私信:私人日记 可以获取相关链接;

大家阅读过程中有哪些看不懂或未尽兴的地方,可以在评论区留言,我会先记下来在后续的教程中找机会再说。

教程有帮助的话请大家帮忙关注、转发、扩散,能不能开专栏还需要你们的支持!