我说的UI程序包括但不限于:Web应用、移动端应用、GUI应用等。
其实简单想一想,UI程序做的事情其实主要分两种:
数据从后台到前台:渲染成人眼能看到的东西。
举个例子:你打开微信,映入眼帘的就是你最近聊天的列表。这些人、头像、聊天记录等,本质上是腾讯存储在数据中心的数据,而这些数据以某种方式,可以是客户端主动请求,也可以是在某种通讯协议下服务端主动推送,总之,数据从“后台”被传输到了“前台”,然后“前台”把这些数据绘制成手机屏幕上的各种样子,把文字渲染出来,把头像放在正确的位置上,等等。
这个例子可以类比到任何UI应用上,就比如我这个博客网站,它虽然是一个纯静态网站,但其实也能分出所谓的“前台”与“后台”:“后台”就是托管这个博客网站的Azure虚机上的文件目录,“前台”就是用户的浏览器以及我写的一些前端代码嘛。“后台”里的静态站点资源文件,以“浏览器请求,然后Nginx负责回应传输”的方式传递到浏览器。然后“前台”,既是用户的浏览器+我写的前端代码,把这些数据渲染成你现在看到的样子。
数据从前台到后台:前台把用户的行为送给后台,
再举个例子:你打开微信的聊天窗口,打了一段文字,点击发送按钮,这个消息就被发到了你好友那边。在这个过程中,“前台”,即是手机上的微信客户端,要收集两个事情:
客户端收集到“行为”与“行为数据”后,会把这些数据传递给“后台”:微信的服务端收到后,才会去执行真正的所谓的“发消息”的行为(执行业务逻辑)。最终把执行的结果再反馈给客户端。
在客户端收到服务端的执行结果后,会把执行结果再渲染在用户的手机屏幕上。这其实也是一个“数据从后台到前台”的过程。
在UI开发过程中,我们一般把这个“行为”叫Event
,而与行为关联的数据,就是所谓的EventArgs
。这两个概念几乎在所有的开发框架里都有,并且几乎都叫"event"和"event args",我这里再举几个例子:
1
所描述的,就是“数据渲染”,2
所描述的,就是"事件处理"。你细想就会发现,所有的UI程序,甚至再宽泛一点,所有与人有交互的计算机程序,其实都是在做这两件事情。
“数据渲染”这件事里,前台的责任最大,这是非常显然的事情。“事件处理”这件事里,前台后台的责任可以说是一半一半:前台要收集事件数据,而后台要处理真正的业务逻辑。
Web应用也是一种UI程序,Web应用做的事情说穿了也是“数据渲染”与“事件处理”,但有意思的是,“前台”和“后台”的定义,在不同的语境下,可能代表的不是一个东西。
以现在流行的前端三架马车React、Vue、Angular来说,再拉上Blazor WASM,如果我们把目光仅局限于前端开发里,那么当我们讨论“数据渲染”和“事件处理”时,前台和后台其实都在用户的浏览器中:
而当我们讨论C/S架构的UI程序和Blazor Server这种东西时,还要带上服务端一起玩的话,“前台”和“后台”的定义,就会被扩展一些:
而换个角度来看,我们在讨论JS前端框架与Blazor WASM的时候,其实也可以把服务端带上,可以把浏览器中的JS进程与服务端两个捏在一起,看作是“后台”
要学习Blazor框架,一定要把这些逻辑概念理清楚了,要明白在不同的语境下,讨论的“前台”与“后台”分别是什么。不然在Blazor WASM和Blazor Server之间反复横跳时,会脑袋抽筋。。我深知上面的逻辑概念都是一些非常浅显的知识,但我同时也确实担心很多人确实没搞清楚。
比如我们已经接触过了,在Blazor组件中,使用@(xxx)
就可以把“变量渲染在页面上”,这句话,在Blazor WASM中,这个“变量”,指的是前端项目中的变量,这个变量传递给浏览器的时候不涉及网络通信。在Blazor Server中,这个“变量”,指的是服务端的那个变量,这个变量要传递给浏览器渲染的话,是可能需要通过websocket执行网络通信的!
而如果我们再认真一点的话,如果你仔细看了前面几篇文章的话,你就会发现在Blazor Server里,“前台”这玩意其实不只是用户的浏览器:如果我们把vdom到浏览器的DOM渲染系统之间的映射一起看作是前台的话,那么Blazor Server里的“前台”囊括了用户的浏览器、运行在浏览器上的客户端程序、服务端的vdom系统、vdom与客户端程序的sync机制这整个一坨。
不过呢,如果上面我说的太绕了,让你感觉不适,也不必要过分纠结,如果我们换个视角来看“数据渲染”与“事件处理”的话,可以简单的把它们理解为:
不用过分纠结什么是前台,什么是后台,我们的变量在哪个位置哪个进程里,我们的回调函数接收的数据是谁发送来的,又历经了多少千山万水。
数据渲染在Blazor WASM里指的是下图中的红色部分
在Blazor Server里指的是下图中的红色部分
书写的语法我们已经很熟悉了,简单来说就是@(xxx)
。比如下面就是一个非常简单的例子,我们在页面上给Razor组件类分别声明三个东西:
code
块中用const
修饰的常量字段,根据C#的语言规范,const
字段的值是编译期已知,且运行期不可更改的。code
块中不带const
修饰的普通字段,根据C#的语言规范,字段需要经过构造函数(或语法糖)进行初始化赋值后方能使用(这里描述并不精确,但无需在此纠结)@{}
块中声明且进行了初始化的本地变量。根据我们之前的介绍,这个变量其实是被声明在Razor类的BuildRenderTree
方法内部的一个临时变量我们在对上述三个变量/字段进行初始化后,对期进行了一次渲染。随后紧接着对两个非常量的变量/字段的值进行更改,进行了二次渲染。代码如下:
@page "/"
@code {
const string constStrField = "This is a const string field";
string strField = "This is a normal string field, initialized during declaration instead of constructor";
}
@{
string strVar = "This is a local variable";
}
@* part 1 : render different type of variables *@
<br />
<p>constStrField == @(constStrField)</p>
<br />
<p>strField == @(strField)</p>
<br />
<p>strVar == @(strVar)</p>
<hr/>
@* part 2 : change the value of variables then render it *@
@{
// this.constStrField = "Can not change the const constant";
this.strField = "Change the value of string field";
strVar = "Change the value of local variable";
}
<br />
<p>constStrField == @(constStrField)</p>
<br />
<p>strField == @(strField)</p>
<br />
<p>strVar == @(strVar)</p>
<hr/>
执行结果如下图所示:
一切看起来非常符合直觉,最符合直觉的是,我们在更改变量/字段的值之前与之后,有两次渲染结果,而两次渲染结果也分别是他们前后两种状态的值。
这看起来非常理所当然,没有什么值得说的。但如果你仔细思考一下就会发现,认真思考“变量的更改”,其实是把我们脑海中,关于“数据渲染”的模型,添加上了时间轴。
什么意思呢?在提起数据渲染的时候,大多数人脑海中会冒出下面这样一张图:
上面这个简单的模型描述的就是@(xxx)
的功能,但仅描述了空间方面上,“内存中的变量/字段”与“页面上显示的东西”之间的关系,上面这个简单的模型图并没有描述出程序是随时间运行的,变量、字段里存储的值是可能改变的,也没有描述出代码的运行的先后顺序。总之就是仅描述了空间上“内存”与“页面”的单向映射,但没有描述时间上这个映射会不会变化,以及以如何规则变化。
而我们上面展示的示例代码的运行逻辑,如果加上时间轴的话,可以画成下面这张图:
这张图画的有点宽,如果看不清的话可以把图片拖到一个浏览器的新Tab里进行观看。上面这张图里的很多细节都是经不起推敲的,是高度简化劣化的,在一个框架使用者,而非研究者的角度,这样去理解是没有什么大问题的。
如果你看得明白上面这张带时间轴的图,就会意识到:@(xxx)
的“数据渲染”,是一次性的:每一次在代码中书写@(xxx)
时,它:
@(xxx)
的那个时刻,对内部变量/字段/表达式进行求值,并把求值结果放进V-DOM中去这意味着,在一次渲染过后,如果变量/字段/表达式的值在内存中发生了变化,那么除非下列两种情况发生,否则新值不会显示在屏幕上:
@(xxx)
在BuildRenderTree
方法返回之前,后续被再次使用 : 并且注意,后续的再次调用只是在屏幕上另外一个地方显示了新值,之前已经渲染出的旧值,是不会改变的。BuildRenderTree
方法被重新执行,这其实也是@(xxx)
被再次执行 : 这其实就是整个页面的“重新渲染”再次强调:“数据渲染”是一次性的,不是“双向绑定”,字段/变量/表达式值在后续发生了变化的话,除非BuildRenderTree
被再次执行,即除非页面重新渲染,否则屏幕上已有的内容不会进行更新。
下面就是一个生动的例子:我们在页面上声明一个本地变量,并对其进行初始化赋值,然后渲染。。有意思的是,我们设定了一个定时器,让这个变量的值每秒都+1。
@page "/"
@{
int num = 73;
var timer = new System.Threading.Timer(
callback: _ => { num++; Console.WriteLine($"num == {num}"); },
state: null,
dueTime: TimeSpan.FromSeconds(1),
period: TimeSpan.FromSeconds(1)
);
}
<h1>num == @num</h1>
对System.Threading.Timer
不熟悉的话,我这里做一下简单介绍:这是dotnet系统类库中,线程库里的一个工具类,它的功能,你可以简单的理解为JS中的setTimeOut
,但细节不太一样。
callback
参数指要定期执行的函数state
是执行callback
函数时要传入的参数,如果callback
函数本身没有参数,就赋null
dueTime
是指,在Timer
实例被初始化创建出来之后,距离第一次执行callback
,之间的延迟时间period
是指后续重复执行callback
时,执行的间歇时间所以上面的代码创建了一个:一秒后正式开始循环执行、每秒都会将本地变量num
的值+1的一个Timer
并且在callback
函数中,我们用Console.WriteLine
将内存中num
的值,输出到“控制台”上。。这里又是一个小知识点:我们现在写的是一个Blazor WASM页面,那么所谓的Console
,其实就是浏览器的调试窗口的Console
。
好了,介绍完毕,下面来欣赏运行结果:
事件处理在Blazor WASM是指下图中的红色部分:
在Blazor Server里指的是下图中的红色部分:
我再次免责声明一下:上图是非常简化劣化的版本,不能深究,但不影响你作为框架的使用者去简单的理解。
事件处理中最重要的一个核心知识点就是:事件处理并不只是对回调函数的调用,还包括后续对BuildRenderTree
的调用。
换句话说:事件回调,会触发页面重新渲染。这是理解Web开发框架中的事件处理的核心。实际上任何UI项目,任何UI框架中的事件处理,都会或多或少的触发“重新渲染”:对于Win32程序来说,是对屏幕的重绘,对于Web项目来说,是对DOM的更新。这里的核心逻辑是这样的:
当然你可能会说:并不是所有的回调函数都会去改变程序的状态,并不是所有的事件处理都有必要重绘UI。
是的,你说的没错,但站在框架设计者的角度上
为了使实际“重绘”的区域尽可能的小,浏览器上的UI框架,包括React/Angular/Vue/Blazor在内的一众框架,殊途同归的选择了同一条技术路线:v-dom
我们可以简单的把v-dom的设计浓缩为两个重点:
重点就在第2点上:框架可能自动化的探测v-dom中的变动,然后非常高效的,在底层渲染的时候,仅对这些变动的部分进行更新,而不是对所有UI都进行更新。
或者换个说法:框架会自动监视v-dom的任何风吹草动,并且以最优化的算法去最高效的保持v-dom与浏览器dom的同步。
话题有一点扯远了,但回到Blazor中,BuildRenderTree
方法的执行,就是在更新v-dom树中的枝桠,所以逻辑上,Blazor中的代码是有四个层次的:
BuildRenderTree
方法会被框架执行BuildRenderTree
的执行会返回一颗子树,然后被嵌接在整个应用的v-dom树中的某个枝桠里话题扯远了,收回一点:对于Blazor框架的使用者来说,现在只需要明确两个关键:
BuildRenderTree
的执行就是渲染这个其实我们之前就稍微接触过,下面是一个简单的例子:
@using Microsoft.AspNetCore.Components.Web
<h3>Counter: @count</h3>
<button @onclick=@(this.IncrCounter)>Incr Counter</button>
@code {
private int count = 1;
private void IncrCounter()
{
this.count++;
}
}
这里有几个要点:
以@
开头的特殊attribute,是为directive attribute,
以@onclick
为例子,虽然它看起来像是HTML标签的一个属性,但实际上它是Razor模板语言中的魔改功能。它其实是一个定义在Microsoft.AspNetCore.Components.Web.EventHandlers
静态类脑门上的一个C# Attribute。
@onclick
的定义大致如下所示:
namespace Microsoft.AspNetCore.Components.Web
// ...
[EventHandler("onclick", typeof(MouseEventArgs), true, true)]
// ...
public static class EventHandlers
{
}
我们不必过分细究上面这特殊的空静态类到底是怎么实现这个魔法的,以及directive attribute到底是如何定义的,只需要知道它是一个定义在C#中的东西就行了
以@onclick
为例,它的值应当是一个EventCallback<MouseEventArgs>
类型的实例。是的,没错,它其实是一个struct模板类。但我们依然不必过分关心一个函数指针是如何被框架包装成一个struct模板类的实例的,我们只需要知道,所谓的EventCallback<xxxx>
其实就是指
xxxx
类型的参数void
由于#1,导致了我们必须在模板文件中引入Microsoft.AspNetCore.Components.Web
这个命名空间后,Razor引擎才能正确识别出@onclick
这种东西是一个directive attribute。
官方模板一般会在_Imports.razor
文件中为所有的Razor模板文件统一引入这个名称空间,所以这个问题一般也不是什么大问题。但如果你是跟我的文章一步步手动创建的项目,那么你就需要注意是否引入了这个名称空间了。
除了@onclick
外,你可以在EventHandlers
文档中找到更多的事件类型
上面我们说了,像@onclick
这种东西,其实是定义在M.A.Components.Web.EventHandlers
静态类脑门上的一个C# Attribute。
这个Attribute本身是Microsoft.AspNetCore.Components.EventHandlerAttribute
,它有两个版本的构造函数,在M.A.Components.Web.EventHandlers
脑门上用到的几乎都是第二个构造函数
public EventHandlerAttribute (string attributeName, Type eventArgsType, bool enableStopPropagation, bool enablePreventDefault);
第一个参数,事件名称,没什么可说的。
第二个参数,是事件回调函数的eventArgs
的类型,也没什么可以的,容易理解。
第三个参数和第四个参数,就牵扯到两个概念:事件的传播与默认行为
框架定义的所有事件,这两个参数的值都是true
:即默认情况下,事件是会传播的,如果有默认行为的话,默认行为也是会被执行的
这是什么意思呢?这就要回到前端的两个小知识点了:
举个粟子:我们用HTML写了一个<input>
元素,它默认是一个文本输入框,它的默认行为是:如果当前的控件焦点在这个文本框上,那么用户在键盘上按什么按键,对应的文字内容就会出现在文本框里。如下图所示:
这就是@onkeypress
事件的默认行为。如果我们想改写这个行为,比如用户每次敲击键盘的时候,让文本框里什么都不做,而只是把用户敲击的键输出在控制台上,你可能会想当然的写下如下代码:
@using Microsoft.AspNetCore.Components.Web
@page "/"
<input @onkeypress=@(this.OnKeyPress)/>
@code {
private void OnKeyPress(KeyboardEventArgs eventArgs)
{
Console.WriteLine($"PRESSED: {eventArgs.Key}");
}
}
你可能想着,我们这样做是不是就改写,或者覆盖了@onkeypress
事件的默认行为了呢?事实上,并没有
你会看到,尽管我们的回调函数确实执行了,但这个事件本身的默认行为依然在按原来的逻辑正常运行,并没有被改写,显然也没有被我们的事件回调覆盖
这是为什么呢?原因在于@onkeypress
的定义中,第四个参数的值是true
:即这个事件默认情况下,是会执行默认行为的:
// ...
[Microsoft.AspNetCore.Components.EventHandler("onkeypress", typeof(Microsoft.AspNetCore.Components.Web.KeyboardEventArgs), true, true)]
// ...
public static class EventHandlers
{
}
而如何阻止默认行为呢?我们需要在事件回调的旁边,再额外写一句声名:@onkeypress:preventDefault=@true
,如下:
这个东西有什么用呢?一般来说,最广泛的用途就是限制用户的输入:比如实现一个只能输入数值的文本框,等之类的。
在浏览器中,事件是与DOM元素挂钩的,这很符合直觉:比如一个按钮遭到了点击,鼠标从一个div上面划过之类的。
而将这个很符合直觉的设计,与DOM元素是树型组织的这个现状结合起来的话,事情就变得有趣了起来:当一个div包含一个button的时候,button遭到点击,是否意味着外层的div也遭到了点击呢?
浏览器的设计实现是:YES!
换个角度来说,就是发生在子元素上的事件,会被传递到父元素上,再传递到爷爷元素上,最终传递到顶。
下面来看一个非常简单的例子:
@using Microsoft.AspNetCore.Components.Web
@page "/"
<div style="border: solid red 1px;width: 200px;height:200px"
@onclick=@(this.RedOnClick)
>
<div style="border: solid green 1px;width: 150px;height:150px;margin: auto;margin-top: 25px"
@onclick=@(this.GreenOnClick)
>
<div style="border: solid blue 1px;width: 100px;height:100px;margin: auto; margin-top: 25px"
@onclick=@(this.BlueOnClick)
>
</div>
</div>
</div>
@code {
private void RedOnClick() { Console.WriteLine("RedOnClick"); }
private void GreenOnClick() { Console.WriteLine("GreenOnClick"); }
private void BlueOnClick() { Console.WriteLine("BlueOnClick"); }
}
它运行起来的样子是这样的:
那么如何阻断事件的传播呢?也很简单,在声明事件回调的旁边,写上类似于@onclick:stopPropagation=@true
就行了
@using Microsoft.AspNetCore.Components.Web
@page "/"
<div style="border: solid red 1px;width: 200px;height:200px"
@onclick=@(this.RedOnClick)
+ @onclick:stopPropagation=@true
>
<div style="border: solid green 1px;width: 150px;height:150px;margin: auto;margin-top: 25px"
@onclick=@(this.GreenOnClick)
+ @onclick:stopPropagation=@true
>
<div style="border: solid blue 1px;width: 100px;height:100px;margin: auto; margin-top: 25px"
@onclick=@(this.BlueOnClick)
+ @onclick:stopPropagation=@true
>
</div>
</div>
</div>
@code {
private void RedOnClick() { Console.WriteLine("RedOnClick"); }
private void GreenOnClick() { Console.WriteLine("GreenOnClick"); }
private void BlueOnClick() { Console.WriteLine("BlueOnClick"); }
}
效果如下:
有个很重要的点需要再次强调:无论是数据渲染,还是事件处理,其实都是数据的单向流动。我们之前写过好几遍“按按键计数器+1”的例子,这种例子会给人一种:数据是双向流动的错觉。
但实际上不是的:之所以在点击了按钮后,页面上的数据会实时更新,是因为框架本身,在事件处理后,将当前组件重新渲染了一遍。
数据的单向传递,辅以框架会在事件处理后自动的重新渲染的机制,其实已经足够了。但唯独对于一种控件,单向数据绑定实现业务逻辑时略显拖沓:文本框。
我们这里举个最简单的例子:
要达到的效果呢,就是用户在文本框中输入用户名时,#3中展示的用户名实时更新。并且当用户点击按钮生成随机用户名时,#1文本框与#3同时更新
@using Microsoft.AspNetCore.Components.Web
@page "/"
Please type username: <input @oninput=@(this.OnInput) value=@(this.username) />
<br/>
<button @onclick=@(this.GenerateUsername)>Generate Random Username</button>
<hr/>
username: @(this.username)
@code {
private string username = "";
private void OnInput(ChangeEventArgs eventArgs)
{
this.username = eventArgs.Value!.ToString()!;
}
private void GenerateUsername()
{
this.username = new string[] { "Alice", "Bob", "Clark", "David", "Eason" }[new Random().Next(5)];
}
}
这里有几个要点:
我们要在内存中存储这个用户名,所以声明了一个字段private string username = ""
这个内存中的用户名,需要渲染在两个地方:一个是最后一行的username: @(this.username)
,另外一个是文本框中的值,以value=@(this.username)
将值渲染为文本框的value
这样,保证了内存中的this.username
一旦发生变动(由事件回调引起的变动),那么页面重新渲染,内存中的值会及时更新至文本框与最后一行上
事件处理方向,我们有两个入口来更改这个用户名:一是文本框本身,使用了oninput
事件,另外一个是随机用户名按钮,使用了onclick
事件
这样,保证了页面上的事件会及时传递给内存
效果如下图所示:
这样写还是略显拖沓,我们可以分析一下我们的逻辑:数据从内存到页面有两条路:
数据从页面(用户行为)到内存也有两条路:
我们可以发现,对于文本框而言,数据既需要保证从内存到页面的流动,也要保证从页面到内存的流动。这里,Blazor框架就为我们提供了一个语法糖:双向绑定
简而言之,双向绑定是把HTML控件的value,与Blazor component中的某个字段作同步绑定,保证任何一头的变动,都能尽快的同步到另外一头。
回到这个例子上,使用了双向绑定后,this.username
的变动就会自动的同步在input.value
里,即页面上文本框展示的内容。同样的,页面上文本框中的内容,即input.value
的任何变动,也会尽快的同步给this.username
如果用双向绑定实现上面的功能,代码如下写:
@using Microsoft.AspNetCore.Components.Web
@page "/"
-Please type username: <input @oninput=@(this.OnInput) value=@(this.username) />
Please type username: <input @bind=@(this.username) />
<br/>
<button @onclick=@(this.GenerateUsername)>Generate Random Username</button>
<hr/>
username: @(this.username)
@code {
private string username = "";
-
- private void OnInput(ChangeEventArgs eventArgs)
- {
- this.username = eventArgs.Value!.ToString()!;
- }
private void GenerateUsername()
{
this.username = new string[] { "Alice", "Bob", "Clark", "David", "Eason" }[new Random().Next(5)];
}
}
但效果有点不尽人意,如下:
可以看到,文本框中的值,仅当在输入完成后,焦点离开文本框,或者按下回车之后,才会传递到内存中去。
为什么会这样呢?这就要回到语法糖的本质了:双向绑定本质上只是一个语法糖,它做的事情是:
input.value
的值赋值给this.username
this.username
的值传递到input.value
上相当于框架背后给你写了一个@onxxx=@(this.OnXXX) value=@(this.username)
。这里关键的问题就来了:这个事件,到底是哪个事件?
答案:这个事件默认情况下是onchange
事件,而onchange
事件并不会随着用户的输入而被激发,它只会在当前控件丢失焦点后才会被激发。
那么紧接着,问题就来了:我们怎么能改写双向绑定默认使用的事件类型呢?答案是:@bind:event
,如下修改:
@using Microsoft.AspNetCore.Components.Web
@page "/"
-Please type username: <input @bind=@(this.username) />
Please type username: <input @bind=@(this.username) @bind:event="oninput" />
<br/>
<button @onclick=@(this.GenerateUsername)>Generate Random Username</button>
<hr/>
username: @(this.username)
@code {
private string username = "";
private void GenerateUsername()
{
this.username = new string[] { "Alice", "Bob", "Clark", "David", "Eason" }[new Random().Next(5)];
}
}
如此修改后,味儿就对了。
一个天然的问题就是:双向绑定只能绑定字符串吗?
答案是:它虽然是个语法糖,但比你想象的要稍微智能那么一点点,至少,它能绑定数值。假如我们把上面例子中的username
的类型改成int,再把随机算法的实现改一改,改成下面这样
@using Microsoft.AspNetCore.Components.Web
@page "/"
Please type number: <input @bind=@(this.number) @bind:event="oninput"/>
<br/>
<button @onclick=@(this.GenerateNumber)>Generate Random Number</button>
<hr/>
number: @(this.number)
@code {
private int number = 0;
private void GenerateNumber()
{
this.number = new Random().Next(9999);
}
}
运行结果如何呢?
结果是:非数值、非合法的int数值,无法输入到文本框中去。。
为什么会这样呢?坦白讲,真实的原因我也不知道,但你可以这样去理解这个双向绑定的语法糖:
this.field
是什么类型,最终都会被字符串化后传递给input.value
this.field = Int.Parse(input.value)
,当然了,具体类型依实际情况而定,但总之它使用的一定是不抛异常的Parse(string)
重载,如果解析失败,直接返回默认值你可以按上面的说法去理解,或者更简单一点:按照直觉去使用双向数据绑定,在99%的场景下,它都是符合直觉的。
DateTime
类型之间的互相转换从上面的描述你能得知,双向绑定至少支持一系列的基础类型,最常用的或许就是字符串、整数、浮点数这三种了。但除此外,Blazor框架单独为DateTime
类开发了一个独立特性。
简单来说,我们可以使用@bind=@(this.field)
将input.value
绑定到一个类型为DateTime
的字段上去,同时为了支持繁杂的日期时间格式,Blazor专门为这个特性开发了一个directive attribute,叫@bind:format
,下面是一个示例:
@using Microsoft.AspNetCore.Components.Web
@page "/"
Please type DateTime in format 'yyyy-MM-ddTHH:mm:ss': <input @bind=@(this.datetime) @bind:format="yyyy-MM-ddTHH:mm:ss"/>
<br/>
<button @onclick=@(this.GetNow)>Get Now</button>
<hr/>
datetime: @(this.datetime)
@code {
private DateTime datetime = DateTime.Now;
private void GetNow()
{
this.datetime = DateTime.Now;
}
}
它的运行效果如下:
@bind:format
的值请参阅这篇文档,可以按需求灵活组合,需要注意的是,它的值影响着数据两个方向流动时的行为:
@bind:format
的值作为format string,来传递给ToString()
方法@bind:format
的值作为format string,来解析输入字符串今天我们介绍了数据在Blazor中是如何流动的,确切的说,是在单个组件内部,数据是如何流动的,其实说白了就三板斧:渲染、事件与双向绑定,其中双向绑定还只是个语法糖,本质上其实只有渲染与事件两个东西。
语法都很简单,重要的是要理解“数据传递”的概念,要意识到无论是渲染还是事件处理,本质上都是数据的流动。并且要注意到所谓的“渲染”的本质,要明白,就目前而言,除了页面初次渲染外,当且仅当有事件被处理时,页面才会被重新渲染。