ASP.NET MVC数据绑定

一、介绍

深入了解 ASP.NET MVC 数据绑定非常重要,特别是对于像富客户端越来越流行,复杂对象需求度也越来越高。我会写一些示例来帮助了解MVC数据绑定的是怎么回事,这对于工作也会事半功倍。

二、键值对

首先我们要明确MVC默认只支持键值对数据绑定(自定义绑定规则除外),了解这一点非常重要,因为对于MVC而言提交时是通过表单。而表单默认就以键值对形式(即 Content-Type:application/x-www-form-urlencoded);当然我们可能提交是带有文件二进制流的 Content-Type:multipart/form-data 形式,我们也可以理解为键值对,只不过编码不同而已。这两种对MVC而言都是一样的。

文本键值对的特点是请求参数都是会以 [name]=[value]& 的形式串连一个字符串。

看到这不必太着急,我相信很多人更想要了解的是通过AJAX来提交一个复杂对象是怎么一回事,但必须先了解键值对,否则脑子很难转过弯来。

三、示例

我们假如以下 Model 是我们遇到的相对复杂的结构,我相信已经足够应付绝大多数环境了。

public class Student
{
    public string Name { get; set; }
    public int Age { get; set; }
    public bool IsCCP { get; set; }
    public byte Status { get; set; }
    public DateTime Created { get; set; }

    /// <summary>
    /// 家庭成员情况
    /// </summary>
    public List<Family> Families { get; set; }

    /// <summary>
    /// 年度分类情况,key:年份
    /// </summary>
    public Dictionary<int, Score> Scores { get; set; }
}

public class Family
{
    public string Name { get; set; }

    public string Mobile { get; set; }
}

public class Score
{
    public int Avg { get; set; }

    public int Highest { get; set; }

    public int Lowest { get; set; }
}

根据Model出现了基础类型、List、Dictionary几乎涵盖所有我们日常需要的类型了,以下相应的前端HTML代码如下:

@using (Html.BeginForm("Index", "Home", FormMethod.Post, new { name = "nForm", id = "nForm", @class = "form-horizontal" }))
{
    <fieldset>
        <legend>基本信息</legend>
        <div class="form-group">
            @Html.LabelFor(model => model.Name, new { @class = "col-sm-2 control-label" })
            <div class="col-sm-10">
                @Html.TextBoxFor(model => model.Name, new { @class = "form-control" })
            </div>
        </div>
        <div class="form-group">
            @Html.LabelFor(model => model.Age, new { @class = "col-sm-2 control-label" })
            <div class="col-sm-10">
                @Html.TextBoxFor(model => model.Age, new { type = "number", @class = "form-control" })
            </div>
        </div>
        <div class="form-group">
            <div class="col-sm-10 col-sm-offset-2">
                @Html.CheckBoxFor(model => model.IsCCP)
                @Html.LabelFor(model => model.IsCCP, new { @class = "" })
            </div>
        </div>
        <div class="form-group">
            @Html.LabelFor(model => model.Status, new { @class = "col-sm-2 control-label" })
            <div class="col-sm-10">
                @Html.DropDownListFor(w => w.Status, new List<SelectListItem>() {
            new SelectListItem(){Text="请选择",Value="0"},
            new SelectListItem(){Text="正常",Value="1"},
            new SelectListItem(){Text="退学",Value="2"}
           }, new { @class = "form-control" })
            </div>
        </div>
        <div class="form-group">
            @Html.LabelFor(model => model.Created, new { @class = "col-sm-2 control-label" })
            <div class="col-sm-10">
                @Html.TextBoxFor(model => model.Created, new { type = "date", @class = "form-control" })
            </div>
        </div>
    </fieldset>
    <fieldset>
        <legend>家庭情况</legend>
        @for (var i = 0; i < Model.Families.Count; i++)
        {
            <div class="form-group">
                @Html.LabelFor(model => Model.Families[i].Name, new { @class = "col-sm-2 control-label" })
                <div class="col-sm-10">
                    @Html.TextBoxFor(model => Model.Families[i].Name, new { @class = "form-control" })
                </div>
            </div>
            <div class="form-group">
                @Html.LabelFor(model => Model.Families[i].Mobile, new { @class = "col-sm-2 control-label" })
                <div class="col-sm-10">
                    @Html.TextBoxFor(model => Model.Families[i].Mobile, new { type = "number", @class = "form-control" })
                </div>
            </div>
            <hr />
        }
    </fieldset>
    <fieldset>
        <legend>分数情况</legend>
        @{int scorePoint = 0;}
        @foreach (var item in Model.Scores)
        {
            <div class="form-group">
                <div class="col-sm-2">
                    <input name="Scores[@scorePoint].Key" value="@item.Key" type="number" class="form-control" />
                </div>
                <div class="col-sm-10">
                    <div class="form-group">
                        @Html.LabelFor(model => item.Value.Highest, new { @class = "col-sm-2 control-label" })
                        <div class="col-sm-10">
                            <input name="Scores[@scorePoint].Value.Highest" value="@item.Value.Highest" type="number" class="form-control" />
                        </div>
                    </div>
                    <div class="form-group">
                        @Html.LabelFor(model => item.Value.Lowest, new { @class = "col-sm-2 control-label" })
                        <div class="col-sm-10">
                            <input name="Scores[@scorePoint].Value.Lowest" value="@item.Value.Lowest" type="number" class="form-control" />
                        </div>
                    </div>
                    <div class="form-group">
                        @Html.LabelFor(model => item.Value.Avg, new { @class = "col-sm-2 control-label" })
                        <div class="col-sm-10">
                            <input name="Scores[@scorePoint].Value.Avg" value="@item.Value.Avg" type="number" class="form-control" />
                        </div>
                    </div>
                </div>
            </div>
            <hr />
            ++scorePoint;
        }
    </fieldset>
    <div class="form-group">
        <div class="col-sm-10 col-sm-offset-2">
            <button type="submit" class="btn btn-primary">Submit</button>
        </div>
    </div>
}

1、基础类型

基本信息都是基础类型,相应的@Html都是非常简单的,我们可以跟踪一下生成的HTML及请求时所提交的相应参数信息:

<input class="form-control" id="Name" name="Name" type="text" value="cipchk">
<input class="form-control" id="Age" name="Age" type="number" value="18">

非常简单的将Name和Age组装成:Name=cipchk&Age=18,我们可以通过 Request.Form 来确认这一点。

2、List类型

家庭情况以List对象存在,相应的@Html只需要通过 for 循环,最后生成的HTML就像:

<!-- 1 -->
<input class="form-control" name="Families[0].Name" type="text" value="父亲">
<input class="form-control" name="Families[0].Mobile" type="number" value="111">
<!-- 2 -->
<input class="form-control" name="Families[1].Name" type="text" value="母亲">
<input class="form-control" name="Families[1].Mobile" type="number" value="222">

和基本类型相比较,重点的name值,会以对象名+下标的形式出现,且这个下标必须是从0逐一递增。

注:原则上我们也可以用foreach来循环,但这出于@Html的运行机制中会以传递的 model => Model.Families[i].Name 做为name值,而这种方式跟我们所需要的键值对名称的约定有出入,所以采用 for 循环方式更符合实际。因为我们可以把以下这句话做为@Html的约束。

对于前端@Html的循环,永远都以 for 来完成。

而最后提交到会组装成数据如:Families[0].Name=父亲&Families[0].Mobile=111&Families[1].Name=母亲&Families[1].Mobile=222

2、Dictionary类型

分数以字典形式出现,其实他也是一个List对象,只不过看起来他只有 Key、Value 而已,所以我们依然可以用 List 的形式来表现,只不过他的表单 name 会更加奇怪一点,像:

<!-- 1 -->
<input name="Scores[0].Key" value="2014" type="number" class="form-control">
<input name="Scores[0].Value.Highest" value="100" type="number" class="form-control">
<input name="Scores[0].Value.Lowest" value="100" type="number" class="form-control">
<input name="Scores[0].Value.Avg" value="100" type="number" class="form-control">
<!-- 2 -->
<input name="Scores[1].Key" value="2015" type="number" class="form-control">
<input name="Scores[1].Value.Highest" value="100" type="number" class="form-control">
<input name="Scores[1].Value.Lowest" value="100" type="number" class="form-control">
<input name="Scores[1].Value.Avg" value="100" type="number" class="form-control">

所需要的表单name和List类型相象,我就不加与赘述。

四、回归HTML标准

查询w3对form描述,键值对这种形和.Net的数据类型之间的互转换原来对于基础类型来说非常简单,我们可以理解为form中的text、number、date等等(大部分是HTML5)这一些都是和我们编程语言的数据类型相之对应,而对于复杂类型对象、List、字典等等,本质也是采用键值对,只不过不同语言对于键的表现形式不同而已。

所以实际我们更应该清楚不同语言在键的表述形式上的理解,那么我相信对于数据绑定这个课题上不会再纠结。

五、AJAX问题

我相信如果了解问题本质以后,这个点就会不存在了,不相信我们改学以上示例,用AJAX提交,只需要这样:

$('form').submit(function () {
    $.ajax({
        url: $(this).attr('action'),
        type: $(this).attr('method'),
        data: $(this).serialize(),
        success: function (res) {

        }
    });
    return false;
});

资料

HTML5之IndexedDB

IndexedDB 是一个为了在客户端中存储结构化数据,也就是我们通常所说的NOSQL。同时也满足一些数据库的特性,比如:大容量、索引检索、事务、安全性、存储类型(包括:二进制对象)、异步API。

基本概念

  • key-values键值对存储:values可以是一个非常复杂的结构体。
  • 事务模式数据库:任何操作都需要一个事务,事务是有生命周期,且只能自动提交。
  • 异步API:虽然也有同步,但是同步目前没有任何一个浏览器版本实现。
  • 同源策略:这里的“源”指的是文档URL所在的协议、域名和端口,在同一个源内的所有数据库都是必须是唯一的。
  • 数据库版本:版本为了区分作数据迁移。
  • object store:同NoSQL中的集合一个意思。

打开数据库

const dbName = 'DemoDb';

let db;
let request = indexedDB.open(dbName, 1);
request.onerror = function  (e) {
    // 错误处理
    alert(e.target.error.message);
}
request.onsuccess = function  (e) {
    // 数据库打开成功处理
    db = request.result;
}
request.onupgradeneeded = function  (e) {
    // 当首次打开数据库或打开的版本号高于当前版本号时执行,同时也是唯一一个最好来创建存储空间、索引、初始数据的地方
}
request.onblocked = function  () {
    // 其他选项卡也打开数据库时
}

可见当我们打开一个数据库时,极为简单。 1是我们的版本号,这里的版本号只允许向上升,而无法对其降级(只能删除数据库)。

onerror IndexedDB被设计来尽可能的减少对错误处理的请求,但是打开数据库会产生错误最有可能会包括:

  • 浏览器会在首次尝试打开一个IndexedDB存储时提醒用户是否允许或拒绝。
  • 隐私模式下访问。
  • 存储设备容量上限。
  • 用户请求清除数据。

而对于错误的处理都是通过事件冒泡方式,从事务再到数据库对象。所以一般如果我们希望处理到所有错误,可以在这里做统一处理。

创建数据库结构

既然前面已经说只能在 onupgradeneeded 事件中创建存储空间,那我们来尝试做一个 customers 存储空间存放客户信息,而每个客户信息包括:唯一编号、姓名、年龄、Email;同时为了加快访问,创建以 name 姓名非唯一索引,以 email 唯一索引(不能重复,否则会收到一个错误消息)。

为了便于查看,我依然放完整代码:

const customerData = [
    { id: 1, name: '苏先生', age: 25, email: 'xx@gmail.com' },
    { id: 2, name: 'asdf', age: 125, email: 'yy@gmail.com' }
];
const dbName = 'DemoDb';

let db;
let request = indexedDB.open(dbName, 1);
request.onerror = function  (e) {
    // 错误处理
    alert(e.target.error.message);
}
request.onsuccess = function  (e) {
    // 数据库打开成功处理
    db = request.result;
}
request.onupgradeneeded = function  (e) {
    // 当首次打开数据库或打开的版本号高于当前版本号时执行
    // 同时也是唯一一个最好来创建存储空间、索引、初始数据的地方

    db = e.target.result;
    // 创建一个存储空间存放customerData
    let objectStore = db.createObjectStore('customers', { keyPath: 'id' });

    // 创建索引
    // 可能会有重复的,因此我们不能使用 unique 索引
    objectStore.createIndex('name', 'name', { unique: false });
    // 希望确保不会有两个相同的email,因此我们使用 unique 索引
    objectStore.createIndex('email', 'email', { unique: true });

    // 初始化存储空间数据
    customerData.forEach(function(item) {
        objectStore.add(item);
    });
}
request.onblocked = function  () {
    // 其他选项卡也打开数据库时
}

增、删、改数据

对象 keypathkey generator

IndexedDB 既然是key-values键值对存储,必然就有键和值才能相关联。然而键的不同会倒置存储值的不同,这取决于对象存储空间是采用 key path 还是 key generator了。

key path

存储的值只能是一个javascript对象,而且对象必须包括和 key path 相同名的属性。比如前面的 let objectStore = db.createObjectStore('customers', { keyPath: 'id' }); 其中是 id 作为 key path,所以在增加对象时必须包含一个叫 id 的属性。

key generator

存储的值就非常自由了,可以是javascript对象或者基础类型(像字符串或数字等等),但是在 put 时就需要指定 optionalKey

事务

如同我们上面说的,每一个操作都需要有一个事务 transaction

db.transaction(['customers'], 'readwrite');

其中第二参数有三个值,代表着不同的事务隔离类型:

readonly:允许读取数据,不改变。
readwrite:允许读取和写入现有数据。
versionchange:允许执行任何操作,包括删除和创建对象存储空间。

使用游标获取所有数据

let listEl = document.getElementById('list');
function loadCustomer() {
    listEl.innerHTML = '';
    let objectStore = db.transaction('customers').objectStore('customers');
    objectStore.openCursor().onsuccess = function(e) {
        let cursor = event.target.result;
        if (cursor) {
            let trEl = document.createElement('tr');
            trEl.innerHTML = `<tr>
                                <td>${cursor.value.id}</td>
                                <td>${cursor.value.name}</td>
                                <td>${cursor.value.age}</td>
                                <td>${cursor.value.email}</td>
                                <td>
                                    <button data-id="${cursor.value.id}" class="view">查看</button>
                                    <button data-id="${cursor.value.id}" class="del">删除</button>
                                </td>
                            </tr>`;
            listEl.appendChild(trEl);
            cursor.continue();
        }
    }
}

openCursor() 包含两个参数:

optionalKeyRange:键的范围,例如:IDBKeyRange.bound("A", "F") 表示 key 的上限为A,下限为F。
optionalDirection:检索的方向,例如:next 表示升序,prev 表示降序。

增改数据

<form action="#" method="post" id="customerForm">
    <p>编号:<input type="number" value="3" id="id" required></p>
    <p>姓名:<input type="text" value="add name" id="name" required></p>
    <p>年龄:<input type="number" value="56" id="age" required></p>
    <p>Email:<input type="email" value="a@qq.com" id="email" required></p>
    <button type="submit" id="save">保存</button>
</form>
let idEl = document.getElementById('id'),
    nameEl = document.getElementById('name'),
    ageEl = document.getElementById('age'),
    emailEl = document.getElementById('email');

document.getElementById('customerForm').onsubmit = function() {
    let idInt = parseInt(idEl.value);
    // 检查id是否存在
    let objectStore = db.transaction(['customers'], 'readwrite').objectStore('customers');

    objectStore.get(idInt).onsuccess = function() {
        var data = this.result || {
            id: idInt,
            name: nameEl.value,
            age: ageEl.value,
            email: emailEl.value
        };
        var updateRequest = objectStore.put(data);
        updateRequest.onsuccess = function() {
            loadCustomer();
        }
        updateRequest.onerror = function(e) {
            alert(this.error.message);
        }
    }
    return false;
}

首先由于我们在创建存储空间时已经指明 id 作为 keypath,所以需要先检索所增加的 id 是否已经存在,存在更新否则新增。

objectStore.put(data) 表示提交一个对象。

注意由于所有API都是异步,因此 updateRequest 需要监听 onsuccess/onerror 事件。而对于 onerror 最有可能出现异步是索引规则,比如我们之前在创建一个email索引是不允许重复,假设插入的数据跟之前的有重复会收到一个错误消息。

删除数据

let req = db.transaction(['customers'], 'readwrite').objectStore('customers').delete(1);
req.onsuccess = function(e) {
    // 删除成功
}

检索数据

根据 key 检索

let id = 1;
let req = db.transaction(['customers'], 'readwrite').objectStore('customers').get(id);

req.onerror = function(e) {
    alert(`未找到 ${id}`);
}
req.onsuccess = function(e) {
    alert(`${id} 的姓名:${req.result.name} `);
}

根据 name 索引检索 name="asdf" 数据

let objectStore = db.transaction('customers').objectStore('customers');
let index = objectStore.index('name');
index.get('asdf').onsuccess = function(e) {
    alert(JSON.stringify(this.result));
}

根据 email 索引检索所有数据

let objectStore = db.transaction('customers').objectStore('customers');
let index = objectStore.index('email');
index.openCursor().onsuccess = function(e) {
    let cursor = e.target.result;
    if (cursor) {
        alert(JSON.stringify(cursor.value));
        cursor.continue();
    }
}

索引的顺序结果和存储对象空间是不一样的。

删除数据库

indexedDB.deleteDatabase("DemoDb");

而相比较 LocalStorage 可存储空间小、同步API。

参考资料

分析插入DOM效率问题:reflow

在富客户端时代,通过JavaScript来改变元素结构是必须的。这是通过DOM或document对象模型来完成操作,而对于当大量操作或者你想写一个比别人更快的应用,那么了解一些对DOM的原理变得尤为重要。

当元素被真正插入到document时会引发浏览器reflow(即重绘元素样式并计算,这是浏览器的事我们不讨论)。比如我们插入一个新元素、改变元素的CSS样式、调整浏览器窗口、访问offsetHeight等等都会引发若干次 reflow。

说了好几次 reflow,那么提高应用效率,减少浏览器触发 reflow 的次数是一个正比关系。

常见几种容易引发 reflow

reflow-chart

从图上看,可以看得出关于一些会引发 reflow 的CSS属性。

一、CSS样式操作

我们应该尽可能的减少对 element.style.* 的操作次数,因为每一行脚本代码都将可能引发 reflow(为什么说可能,因为对于像现代浏览器会对这种操作做适当的优化)。

function setLinkStyle() {
    element.style.fontWeight = 'bold';
    element.style.color = '#000';
    // 更多类似
}

更适合的方式应该是

.link { font-weight: bold; color: #000; }
function setLinkStyle() {
    element.className = 'link';
}

二、尽可能处理好DOM后再插入

这种创建或修改一个也会引发一个 reflow。假如我们有个函数用来创建新元素,链接文本为 Text;并提供 classlink

function addLink(parentElement) {
    var element = document.createElement('a');
    parentElement.appendChild(element);
    element.innerHTML = 'Text';
    element.className = 'link';
}

这将会引发三次 reflow。因此,我们更快的写法应该是:

function addLink(parentElement) {
    var element = document.createElement('a');
    element.innerHTML = 'Text';
    element.className = 'link';
    parentElement.appendChild(element);
}

三、DocumentFragment 方法

DocumentFragment 他是一个轻量级且不需要父代的DOM对象,他对浏览器或手机浏览器的兼容支持非常完美。

比如我们连续创建30个链接,比如:

function addBatchLink(parentElement) {
    for (var i = 0; i < 30; i++) {
        var element = document.createElement('a');
        element.innerHTML = 'Text';
        element.className = 'link';
        parentElement.appendChild(element);
    }
}

这里会引发30次 reflow。而通过 DocumentFragment 的方式只会触发一次 reflow

function addBatchLink(parentElement) {
    var fragment = document.createDocumentFragment();
    for (var i = 0; i < 30; i++) {
        var element = document.createElement('a');
        element.innerHTML = 'Text';
        element.className = 'link';
        fragment.appendChild(element);
    }
    parentElement.appendChild(fragment);
}

四、offsetHeight 等属性

appendChild 一个元素后接着再访问元素的 offsetHeight 样式属性值时也会立刻触发一个 reflow,且脚本会等待 reflow 完成后才会继续,虽然计算会非常快。

jQuery的DOM操作

jQuery对DOM操作的核心文件是 manipulation.js,这里扩展了一些像 appendafterbefore 等关于DOM操作接口。而这里的核心是 domManip() 方法,他会使用 createDocumentFragment 创建一个全新元素对象,最后才执行一次 appendChild 插入元素。

可见jQuery已经做了很多优化。

重绘与reflow

浏览器会引起重绘,可能会是当添加元素、改变元素可见性、改变背景颜色等时。

而reflow是当DOM被改变风格(我们大可指CSS)时会执行,当改变className、浏览器窗口大小等会进行若干次 reflow,当父元素执行 reflow时,也一并会考虑到子元素的变化,甚至前后代元素,最后变成所有都需要 reflow

重绘与reflow,他们就像是相互关联着,都是影响 DOM 操作效率最主要原因。

影响 reflow 时间

  • DOM结构深度越深,修改元素后代越多所需要时间越长。
  • 最小化CSS规则、删除未使用的CSS,尽可能使用className。

总结

关于 reflow 的话已经说了那么多,如果我们抛开UI阻塞、重绘等问题外,对于SPA应用而言操作DOM的效率影响者整个应用的效率。了解为什么 reflow 是一个至关重要问题,这样才能写出更高效的前端代码。

参考资料

html5的设计

其实是在Jeremy Keith在 Fronteers 2010上的主题演讲,我只是做了一些归纳:
伯斯塔尔法则:发送时要保守;接收时要开放。

避免不必要的复杂性

1、简化doctype为:<!DOCTYPE html>
2、头部信息中发送字符编码简化为:<meta charset=”utf-8″>
3、CSS引用可以不需要type。
4、script可以不需要type。

支持已有的内容

1、支持大小写名称。
2、属性值可以不加引号。
3、标签可不闭合。
比如以下四段都是合法的:

<img src="foo" alt="bar" />
 <p>Hello world</p>
 <img src="foo" alt="bar">
 <p>Hello world
 <IMG SRC="foo" ALT="bar">
 <P>Hello world</P>
 <img src=foo alt=bar>
 <p>Hello world</p>

解决现实的问题

链接包含块级元素,这点也是我的最爱。

内容模型

新增四个内容模型:头部(header)、脚部(footer)、分区(section)、文章(article)。
而像footer未必要放在底下,最重要的是它们的语义,跟位置没有关系。
在HTML5中,只要你建立一个新的内容块,不管用section、article、aside、nav,还是别的元素,都可以在其中使用H1,而不必担心这个块里的标题在整个页面中应该排在什么级别;H2、H3,都没有问题。
html5的vedio

<video>
 <source src="movie.mp4">
 <source src="movie.ogv">
 <object data="movie.swf">
 <a href="movie.mp4">download</a>
 </object>
 </video>

上面的代码中包含了4个不同的层次。
1、如果浏览器支持video元素,也支持H264,没什么好说的,用第一个视频。
2、如果浏览器支持video元素,支持Ogg,那么用第二个视频。
3、如果浏览器不支持video元素,那么就要试试Flash影片了。
4、如果浏览器不支持video元素,也不支持Flash,我还给出了下载链接。

一旦遇到冲突,最终用户优先,其次是作者,其次是实现者,其次标准制定者,最后才是理论上的完满。

让IE支持HTML5标签

HTML5标签无法在IE下面无法被识别,自然也就无法渲染样式,应该来讲是IE8以下的版本。好了,今天说的HTML5 Shiv主要作用就是让IE正常识别HTML5标签,这个方法是来源于http://code.google.com/p/html5shiv/。但是注意,这只里只强制IE正常的渲染样式,其标签自身功能还是无法呈现的。

HTML5 Shiv原理

IE下如果遇到无法识别的标签,那只要在DOM创建前使用document.createElement就可以强制渲染该标签样式。例如:

<html>
<head>
<style>nav a { color:#F50; }</style>
</head>
<body>
<nav><a href="home.html">Home</a></nav>
</body>
</html>

在IE下面是链接颜色是无法渲染的。

HTML5 Shiv使用

只要在head下(必须是head)引用以下脚本,那么IE就能正常渲染样式。

<!--[if lt IE 9]><script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script><![endif]-->

© 2017 卡片机色彩 沪ICP备13032872号-3

Theme by cipchk

to top