使用DotVVM实现ASP.NET中的前端数据绑定

DotVVM是一个免费(也有可试用的商业增强版本)的ASP.NET框架,可用于ASP.NET MVC/Core。它提供了一套MVVM(Model-View-ViewModel)框架,可以用于替代Vue.js或Knockout(DotVVM本身就是基于Knockout),用亲近ASP.NET的语法,实现前端数据绑定。

前端数据绑定

在ASP.NET MVC中,我们从后端取得一些数据到前端渲染,或是从前端输入一些数据存到后端,都需要手写代码处理这些数据。

比如,我们要渲染一个表格,可能要这样(Razor语法,出自SkyDriftCoreWeb):

        @for (int i = 0; i < Model.Records.Count; i++)
        {
            var item = Model.Records[i];
            var u = (await Core.UserManager.GetUserByIdAsync(item.UserId));

            <tr>
                <td>@(i + 1)</td>
                <td>
                    @{
                        if (u == null)
                        {
                            <text> </text>
                        }
                        else
                        {
                            @Html.ActionLink(u.NickName ?? u.UserName, "Info", new { userId = u.Id })
                        }
                    }
                </td>
                <td>@Helper.TimeToString(item.TotalTime)</td>
            </tr>
        }

在上面的例子中,我们用for语句把每条数据取出来,再以合适的格式插入到HTML标签中。如果我们需要改变这个表格,例如表格中条目很多,我们每次只显示10条,点击下一页再显示之后的10条,那么我们只能通过刷新整个页面,或是AJAX等方式来实现。

(当然,你可以使用Vue.js、Knockout等实现,那就跟DotVVM所实现的功能差不太多了,不过DotVVM仍然具有语法上的优势。) 

想想我们在WPF(MVVM)中是怎么做的:

<DataGrid ItemsSource="{Binding Records}">
  <DataGrid.Columns>
    <DataGridTextColumn Binding="{Binding User}"/>
    <DataGridTextColumn Binding="{Binding Time, StringFormat={}{0:s}}"/>
  </DataGrid.Columns>
</DataGrid>

我们将表格绑定到一个对象的List,将列绑定到列表中的对象的属性。这样,在显示时,控件就能通过对应的数据自动显示这个表格。更重要的是,当List发生改变(增删条目)时,显示的表格也能够自动刷新以反映这些改变。 

使用DotVVM,你就可以用这种类似的MVVM方式来实现网页上的表格了:

<dot:GridView DataSource="{value: Records}" class="page-grid">
            <Columns>
                <dot:GridViewTextColumn ValueBinding="{value: User}" />
                <dot:GridViewTextColumn ValueBinding="{value: Time}" />               
            </Columns>
</dot:GridView>

 

这个表格会在你的后端(ViewModel)中的方法改变了List(Records)之后进行页面的局部更新,因此无需再手写AJAX了。

 

对于输入而言也存在类似的情况。在纯净的ASP.NET MVC中,我们获取一个文本框的输入,要在POST之后,在这个POST的处理方法中,从传入的Model中找到这个输入,然后开始进行处理。例如下面的代码(同样来自SkyDriftCoreWeb):

[HttpPost]
public async Task<IActionResult> Add(AuthoriseSerialModel request)
{	
	var user = new ApplicationUser
	{
		UserName = request.account,
		Email = request.account,
		RegisterTime = DateTime.Now,
	};
	var result = await _userManager.CreateAsync(user, request.password);
	if (result.Succeeded)
	{
		await db.SaveChangesAsyncLock();
		return Json(new { user_id = user.Id.ToString() });
	}
	return Json(new { user_id = AuthoriseSerialModel.InvaildInput.ToString() });
}

这段代码可以看出,前面几行我们都在做一个数据的搬运工,生成一个后端用的Model,然后将表单中的输入搬运进去。随后我们才能开始处理操作。

而在DotVVM中,我们可以使用绑定:

<dot:TextBox Text="{value: SearchId}" class="form-control" />
<dot:LinkButton class="btn input-group-addon" Click="{command: SearchForSong(SearchId)}">

上面的代码会在前端显示为一个文本框和一个按钮,其中文本框的输入绑定到了ViewModel.SearchId属性(string)。当你在TextBox中输入一串字符,随后TextBox失去焦点[1]之时,你的输入已经自动更新到了ViewModel.SearchId。当我们点击按钮时,会执行ViewModel.SearchForSong方法[2],此时ViewModel.SearchId已经是更新过的值,因此ViewModel.SearchForSong方法可以直接取ViewModel.SearchId进行操作,我们也无需再做数据的搬运工了。

[1]这与WPF是一致的,对于默认情况下的数据绑定,大部分控件会在失去焦点(LostFocus)后发送通知。但是这是可以改变的,比如你可以修改为一旦字符发生改变就更新ViewModel中的值,不过这样通常很影响性能,在Web中也会消耗更多流量。但是也有这样的应用场景,比如谷歌、百度等搜索引擎当你键入第一个字的时候就已经开始搜索并显示对应结果的下拉框了。

[2]这也是MVVM/DotVVM的特色:命令绑定,也是会让诸位写惯了WPF/WinForm的朋友最亲切的地方之一,毕竟在纯净的ASP.NET中,你需要这么写:

<form asp-controller="Account" asp-action="Register" asp-route-returnurl="@ViewData["ReturnUrl"]" method="post">

 

DotVVM页面与控件

平心而论,Web后端开发的三大门派ASP.NET/JSP/PHP中,View页面语法最简洁的必然是ASP.NET的Razor语法,参见上文中的@for,比什么<c:for>不知高到哪里去了。

DotVVM为了其实现单独设计了一套View写法,也就不能使用Razor了,不过只要安装了DotVVM的VS插件就依然能有良好的语法提示和模板支持。DotVVM的页面后缀为.dothtml。

@viewModel Program.ViewModels.DefaultViewModel, Program
@masterPage Views/MasterPage.dotmaster
@import Program.Resources

<dot:Content ContentPlaceHolderID="MainContent">
    <div></div>
</dot:Content>

与Razor页面非常相似,前面以@开头的几行分别为指定ViewModel类型、指定模板页、导入其他内容(这里是导入资源)。

随后就开始HTML部分,除了可以写常规的HTML外,还可以写DotVVM扩展的元素。DotVVM的控件均以dot、bp(BusinessPack)、bs(Bootstrap扩展)命名空间开头。

DotVVM的控件在前端显示时均会渲染成带有Knockout绑定的HTML元素,因此大部分控件也能够写与标准HTML元素一样的属性(如class,title,ID等)。在控件中使用大括号(类似于WPF)标识绑定内容,如{value: Song.Name}意为绑定到ViewModel.Song.Name,{command: AddSong(Song.Name)}意为绑定到ViewModel.AddSong(ViewModel.Song.Name)。此外绑定中还支持一些运算符(如:!IsTrue,1+2,Name=='HYBRID'等,注意字符串使用单引号)。在HTML元素中也可以写某些类似的绑定,此时要使用双大括号,例如<div>{{value: Song.Name}}</div>可将Song.Name渲染在DIV中。详情请查阅DotVVM官方文档。

(有一点要吐槽的是,由于开发迭代太快,官方文档有很多页面已经404了,严重影响用户心情。)

在后端方面基本就是写ViewModel和Model,没有什么特别的,需要注意了解Context类中的静态方法,比如要取得当前连接的上下文(比如Request的IP地址)就要用到(Core)Context.GetAspNetCoreContext()或者(MVC)Context.GetOwinContext()。另外就是可以重载PreRender方法在页面渲染前进行某些处理,把ViewModel中的属性值初始化,以保证页面显示的初始值正确。

开发中容易遇到的问题

写了一个页面,可是所有的绑定都没生效

这可能是你的ViewModel逻辑有错误,DotVVM在绑定阶段就遇到了错误,因此整个绑定都没有成功。例如你有绑定过ViewModel.Song.Name,而实际上Song为null,就会出现这种问题。

我在方法中更新了一个属性值,而页面上没有立即刷新

有几点需要注意:

1. WPF中,属性靠OnPropertyChanged事件通知更新,所以当你改变属性值时,通知立马发送出去了。而在DotVVM中,只有你方法运行完了,才会随着Response发送改变后的ViewModel,向前端通知更新。比如你在方法中改变了ViewModel.Song.Name,然后下一行代码是await Task.Delay(10000);或者Thread.Sleep(10000);,那么你只有在至少10秒之后才能看到前端页面的更新了。所以你应当想办法把费时的步骤分离出来,让不��时的方法执行完后,再去触发费时的方法。但是目前似乎没有什么特别好的触发方法。DotVVM团队已计划在今后加入Timer等控件解决这些问题。

2. 接上一条,刷新慢还好说,有时候你会发现,方法执行完了也压根没有刷新。原因是,如果你在方法中调用了Context类下的方法,这些方法可能会改变原本的返回步骤。比如Context.ReturnFile是一个非常常用的让用户下载文件的方法,但是一旦使用这个方法作为你的处理方法的结尾(也只能是结尾),你会发现这个方法中对属性的改变都不会刷新了。这是因为Context.ReturnFile的实现原理是跳转到了一个别的地址(原文:the user is redirected to a special URL that returns the file to him),所以当前页面的值没被更新。解决方法同上一条,把ReturnFile看做一个费时的方法。

3. DotVVM中的绑定也是有方向的(类似于WPF),使用[Bind(Direction)]标签来指定绑定方向,大部分控件默认是双向。如果你指定了Direction.ServerToClient系列的枚举值,那么前端的更改不会更新到后端,而后端的更改会更新到前端(这有助于防止用户恶意修改前端的值来引发后端错误);反之,Direction.ClientToServer系列会导致后端的更改不同步到前端,不过这个真的很少用。

混合模式程序集是针对“v2.0.50727”版的运行时生成的……

这个与DotVVM无关,是本次开发中遇到的ASP.NET Core的问题。准确地说,是ASP.NET Core on .NET Framework的问题。我在这个目标平台为netfx471项目中引入了一个.NET2.0的库,然后使用了自托管,当程序运行时,一旦调用到2.0的库便会出现这个异常。在标准的.NET程序中,我们可以在app.config中加入以下配置项来解决这一问题:

  <startup useLegacyV2RuntimeActivationPolicy="true">
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7" />
  </startup>

但是这是一个Web程序,我们知道在Web程序中对应app.config的是web.config,而且当你在VS中新建项的时候,也可以看到选项中有新建WEB配置文件(web.config)而没有app.config。那么我们在web.config中加入以上语句运行,结果是……无效!

随后我想到,这毕竟是自托管啊,我们最后运行的是.exe,那么果然我们还是应该把上述配置加到app.config?但是VS没有这个选项啊?!

此时就需要果断执行以下操作:web.config ->改名!-> app.config。

然后编译运行,大功告成。

值得注意的是,即使项目中没有app.config,VS在编译时也会自动生成.exe.config,其内容是各个依赖库的版本重定向。在我们向项目中强行加入app.config之后,编译生成的.exe.config文件自动融合了我们的配置与之前的版本重定向内容。注意不要试图直接将配置加在.exe.config中,因为VS每次都会重新生成此文件。另外你仍然需要使用web.config来配置Web相关的属性(如MIME type),只是平时在.NET应用程序中用的配置要在app.config中配置,两个配置文件可以并存。

 

添加评论

Loading