FineUI v3.3.0 更新的内容非常多,所以一下子从 v3.2.6 连跳 3 个小版本,直接来到了 v3.3.0。详细的更新记录请参考这里:http://fineui.com/version

主要的更新有如下几个方面:

  1. 外置ExtJS库
  2. 去AXD化
  3. 表格合计行
  4. 表格可编辑单元格的增删改
  5. 顶部菜单框架

下面就来详细说明这些更新。

1. 外置ExtJS库

FineUI 最初使用的是 GPL v2 授权协议,不过这和 FineUI 所倡导的开源免费的原则相抵触,因为如果某个企业使用了 FineUI 库,即使已经购买了 ExtJS 的商业授权,还是需要公开源代码的,因为受到 FineUI 的 GPL v2 协议限制。基于这个原因,FineUI 从 v3.1.0 开始拥抱 Apache License 2.0,从而真正做到了免费开源!

上面这个转变过程,我曾经写过一篇博客记录:

不仅开源,而且对企业应用完全免费!ExtAspNet弃用GPL v2,拥抱Apache License 2.0

然而,在详细阅读了 ExtJS 的授权协议后,我发现 FineUI 并没有完全遵守 ExtJS 所指定的规则,先来看看 ExtJS 的对开源工具的制定的规则:

ExtJS Open Source License

Sencha is an avid supporter of open source software.Our open source license is the appropriate option if you are creating an open source application under a license compatible with theGNU GPL license v3. Although the GPLv3 has many terms, the most important is that you must provide the source code of your application to your users so they can be free to modify your application for their own needs.

If you would like to use the GPLv3 version of Ext JS with your non-GPLv3 open source project, the following FLOSS (Free, Libre and Open Source) exceptions are available: 

Open Source License Exception for Development

虽然 FineUI 使用的 Apache License 2.0 是和 GPL v3兼容的协议,不过 ExtJS 还制定了更加严格的规则:不能包含 ExtJS 的源代码,而是要告诉用户怎么获取 ExtJS 的源代码!

FineUI 作为知名的开源软件,会无条件遵守开源社区的游戏规则,因此在本次 v3.3.0 中做出重大调整:

  • FineUI 的 Apache License v2.0 授权协议 与 ExtJS 的 GPL v3 兼容;
  • FineUI 公开全部源代码,没有任何保留;
  • FineUI 不包含 ExtJS 的任何源代码;
  • FineUI 不将 ExtJS 作为整体发布,而是提供获取 ExtJS 的方法;
  • FineUI 公开说明使用了 ExtJS 库,并指出 ExtJS 库是采用 GPL v3 授权协议的;
  • FineUI 是为了将 ExtJS 引入 ASP.NET 领域,而非独立存在的库。

如果获取适用于 FineUI 的ExtJS 库呢?

  1. 首先下载 ExtJS 库:http://www.sencha.com/products/extjs3/download/
  2. 将 ExtJS 库的全部内容拷贝到官方示例目录:FineUI.Examples\extjs_builder\extjs_source_all
  3. 运行 build.bat,即可生成目录:FineUI.Examples\extjs
  4. 将生成的 extjs 拷贝到你网站的根目录下即可(老项目不需要修改任何代码和配置文件!)。

注:由于 ExtJS 库比较大(20M),我们在官方论坛提供了生成好的 extjs 目录方便大家使用:http://fineui.com/bbs/forum.php?mod=viewthread&tid=3218

2. 去AXD化

外置 ExtJS 库带来了另一个好处,再也不用使用散落在网站各处的 res.axd 路径了(为了保证老项目的正常运行,之前 res.axd 的方式仍然有效)!

AXD 是 ASP.NET 内置的一种获取程序集内部资源的方式,但是在实际部署中会出现各种问题,在官方论坛 AXD + 404 的总结帖子就有好几个:

snap241

其中最典型的错误是在 IIS 中没有设置正确的 AXD 扩展:

013821xcx8awppppxlpptp

更离谱的是,错误的服务器时间也会导致 AXD 出现 404 错误,具体原因不明:

http://fineui.com/bbs/forum.php?mod=viewthread&tid=1271

http://www.cnblogs.com/huangtailang/archive/2011/03/29/1999175.html

从 FineUI v3.3.0 开始,只要你不手工调用 res.axd 路径,就再也不会出现上述问题了。

3. 表格合计行

论坛用户对表格合计行的呼声特别高,实际项目中可能需要对当前分页数据合计,也可能对全部数据合计。

这次更新,我们特别制作了几个示例,由于需要手写 CSS 和 JavaScript ,所以对程序员的要求比较高,不过没关系大家只需照例子写就行了。

3.1 服务器全部合计

snap242

实现上述效果,需要分三步走:

1. 在后台代码中生成合计数据

1:protected void Page_Load(object sender, EventArgs e) {
2:    if (!IsPostBack) {
3:        BindGrid();
4: 
5:        OutputSummaryData();
6:    }
7:}
8: 
9: 
10:private void OutputSummaryData() {
11:    DataTable source = GetDataTable2();
12: 
13:    float donateTotal = 0.0f;
14:    float feeTotal = 0.0f;
15:    foreach(DataRow row in source.Rows) {
16:        donateTotal += Convert.ToInt32(row["Donate"]);
17:        feeTotal += Convert.ToInt32(row["Fee"]);
18:    }
19: 
20:    JObject jo = new JObject();
21:    jo.Add("donateTotal", donateTotal);
22:    jo.Add("feeTotal", feeTotal);
23: 
24:    hfGrid1Summary.Text = jo.ToString(Newtonsoft.Json.Formatting.None);
25: 
26:}

由于合计数据在不改变数据源的情况下是不变的,因此我们只要在第一次页面加载(!IsPostBack)时生成合计数据即可。

然后将全部合计数据以 JSON 字符串的形式保存到隐藏字段(HiddenField)中,供前台 JavaScript 代码调用。

snap243

2. 使用前台代码显示合计数据

1:<script>
2:    var gridClientID = '<%= Grid1.ClientID %>';
3:    var gridSummaryID = '<%= hfGrid1Summary.ClientID %>';
4: 
5:    function calcGridSummary(grid) {
6:        var donateTotal = 0,
7:            store = grid.getStore(),
8:            view = grid.getView(),
9:            storeCount = store.getCount();
10: 
11:        // 防止重复添加了合计行
12:        if (Ext.get(view.getRow(storeCount - 1)).hasClass('mygrid-row-summary')) {
13:            return;
14:        }
15: 
16:        // 从隐藏字段获取全部数据的汇总
17:        var summaryJSON = JSON.parse(X(gridSummaryID).getValue());
18: 
19: 
20:        store.add(new Ext.data.Record({
21:            'major': '全部合计:',
22:            'donate': summaryJSON['donateTotal'].toFixed(2),
23:            'fee': summaryJSON['feeTotal'].toFixed(2)
24:        }));
25: 
26: 
27: 
28:        // 为合计行添加自定义样式(隐藏序号列、复选框列,取消 hover 和 selected 效果)
29:        Ext.get(view.getRow(storeCount)).addClass('mygrid-row-summary');
30: 
31:    }
32: 
33:    // 页面第一个加载完毕后执行的函数
34: 
35:    function onReady() {
36:        var grid = X(gridClientID);
37:        grid.addListener('viewready', function () {
38:            calcGridSummary(grid);
39:        });
40: 
41:    }
42: 
43:    // 页面AJAX回发后执行的函数
44: 
45:    function onAjaxReady() {
46:        var grid = X(gridClientID);
47:        calcGridSummary(grid);
48:    }
49:</script>

上面代码首先定义了一个向表格中添加合计行的函数(calcGridSummary),并分别在页面第一次加载时(onReady)和AJAX结束时(onAjaxReady)调用此函数。

在 calcGridSummary 函数内部,通过 JSON.parse 函数解析保存在隐藏字段中的合计数据,然后调用表格的 grid.getStore().add 来添加合计行,最后给这个合计行添加 CSS 样式(mygrid-row-summary)。

上面的代码不大完善,新增加的合计行属于表格数据的一部分,因此用户可以选中这个合计行,这是我们所不希望的,怎么办?

1: function onReady() {
2:     var grid = X(gridClientID);
3:     grid.addListener('viewready', function () {
4:         calcGridSummary(grid);
5:     });
6: 
7:     // 防止选中合计行
8:     grid.getSelectionModel().addListener('beforerowselect', function (sm, rowIndex, keepExisting, record) {
9:         if (Ext.get(grid.getView().getRow(rowIndex)).hasClass('mygrid-row-summary')) {
10:             return false;
11:         }
12:         return true;
13:     });
14: }

我们还需要在页面初始化时,加入防止合计行被选中的事件处理,其中用到了刚刚添加到合计行的 CSS 定义(mygrid-row-summary)。

3. 使用 CSS 调整合计行样式

最后,我们还需要通过 CSS 来简单调整合计行的样式:

1:<style>
2:    .mygrid-row-summary.x-grid3-row {
3:        background-color: #efefef !important;
4:        background-image: none !important;
5:        border-color: #fff #ededed #ededed !important;
6:    }
7:    .mygrid-row-summary.x-grid3-row .x-grid3-td-numberer, .mygrid-row-summary.x-grid3-row .x-grid3-td-checker {
8:        background-image: none !important;
9:    }
10:    .mygrid-row-summary.x-grid3-row .x-grid3-td-numberer .x-grid3-col-numberer, .mygrid-row-summary.x-grid3-row .x-grid3-td-checker .x-grid3-col-checker {
11:        display: none;
12:    }
13:    .mygrid-row-summary.x-grid3-row td {
14:        font-size: 14px;
15:        line-height: 16px;
16:        font-weight: bold;
17:        color: red;
18:    }
19:</style>

3.2 服务器分页合计

服务器分页合计和服务器全部合计的前台代码完全相同,所不同的时分页合计时每次表格数据绑定都需要计算本页的合计数据,如下所示:

1:private void OutputPageSummaryData(DataTable source) {
2:    float donateTotal = 0.0f;
3:    float feeTotal = 0.0f;
4:    foreach(DataRow row in source.Rows) {
5:        donateTotal += Convert.ToInt32(row["Donate"]);
6:        feeTotal += Convert.ToInt32(row["Fee"]);
7:    }
8: 
9:    JObject jo = new JObject();
10:    jo.Add("donateTotal", donateTotal);
11:    jo.Add("feeTotal", feeTotal);
12: 
13:    hfGrid1Summary.Text = jo.ToString(Newtonsoft.Json.Formatting.None);
14: 
15:}
16: 
17:private void BindGrid() {
18:    // 1.设置总项数(特别注意:数据库分页一定要设置总记录数RecordCount)
19:    Grid1.RecordCount = GetTotalCount();
20: 
21:    // 2.获取当前分页数据
22:    DataTable table = GetPagedDataTable(Grid1.PageIndex, Grid1.PageSize);
23: 
24:    // 3.绑定到Grid
25:    Grid1.DataSource = table;
26:    Grid1.DataBind();
27: 
28:    // 输出分页合计结果
29:    OutputPageSummaryData(table);
30:}

页面效果如下:

snap244

3.3 服务器全部合计(绝对定位合计行)

实际项目的一个常见需求是将合计行绝对定位到表格底部,如下图所示:

170118ulqfnvbzyw1clyck

该如何实现这个功能?

这个时候我们只好在前台下工夫了,总的思路如下:

1. 和服务器全部合计一模一样的前台代码;

2. 将生成的合计行拷贝一份,然后将拷贝的合计行插入表格容器节点中并绝对定位。

一定要注意:在这个过程中,是要拷贝一个合计行(而不是删除合计行),这样才不至于在滚动条滚动时把最后一行表格数据遮挡住。此时页面上其实是有两个一模一样的合计行,只不过所在位置不同,并且原始的合计行要设置 CSS 属性 visibility: hidden;(让原始的合计行占位,但不显示出来,这个主意是不是很妙眨眼)。

看看下图就明白了:

snap245

关键 JavaScript 代码:

1:function calcGridSummary(grid) {
2:    var donateTotal = 0,
3:        store = grid.getStore(),
4:        view = grid.getView(),
5:        storeCount = store.getCount();
6: 
7:    // 防止重复添加了合计行
8:    if (Ext.get(view.getRow(storeCount - 1)).hasClass('mygrid-row-summary')) {
9:        return;
10:    }
11: 
12:    // 从隐藏字段获取全部数据的汇总
13:    var summaryJSON = JSON.parse(X(gridSummaryID).getValue());
14: 
15: 
16:    store.add(new Ext.data.Record({
17:        'major': '全部合计:',
18:        'donate': summaryJSON['donateTotal'].toFixed(2),
19:        'fee': summaryJSON['feeTotal'].toFixed(2)
20:    }));
21: 
22: 
23:    // 为合计行添加自定义样式(隐藏序号列、复选框列,取消 hover 和 selected 效果)
24:    var summaryNode = Ext.get(view.getRow(storeCount)).addClass('mygrid-row-summary');
25: 
26:    // 找到合计行的外部容器节点
27:    var viewportNode = summaryNode.parent('.x-grid3-viewport');
28:    // 删除容器节点下直接子节点为 mygrid-row-summary 的节点
29:    viewportNode.select('> .mygrid-row-summary').remove();
30: 
31:    // 创建合计行的副本
32:    var cloneSummaryNode = summaryNode.dom.cloneNode(true);
33:    // 修改合计行的副本的样式,绝对定位,距离底部0px,显示副本(默认是占位隐藏 visibility: hidden;)
34:    Ext.get(cloneSummaryNode).setStyle({
35:        position: 'absolute',
36:        bottom: 0,
37:        visibility: 'visible'
38:    });
39: 
40:    // 向容器节点添加合计行的副本
41:    viewportNode.appendChild(cloneSummaryNode);
42: 
43:}

更加详细的代码,请直接去看官方示例:http://fineui.com/demo/#/demo/grid/grid_summary_absolute.aspx

4. 表格可编辑单元格的增删改

论坛用户对 ExtJS 可编辑功能的呼声也很高,虽然 FineUI 的模板列能够实现一定的编辑功能(http://fineui.com/demo/#/demo/grid/grid_edit.aspx),但毕竟不是 ExtJS 的原生方式。

上个版本简单实现了可编辑表格的“改”,这个版本对此进行了修正和改进,下面就来一一描述。

4.1 可编辑表格的“改”

snap246

首先来看下 ASPX 文件的结构定义:

1:<x:Grid ID="Grid1" ShowBorder="true" ShowHeader="true" Title="表格" Width="850px" Height="350px"
2:    runat="server" DataKeyNames="Id,Name" AllowCellEditing="true" ClicksToEdit="1">
3:    <Columns>
4:        <x:TemplateField Width="60px">
5:            <ItemTemplate>
6:                <asp:Label ID="Label1" runat="server" Text='<%# Container.DataItemIndex + 1 %>'></asp:Label>
7:            </ItemTemplate>
8:        </x:TemplateField>
9:        <x:RenderField Width="100px" ColumnID="Name" DataField="Name" FieldType="String"
10:            HeaderText="姓名">
11:            <Editor>
12:                <x:TextBox ID="tbxEditorName" Required="true" runat="server">
13:                </x:TextBox>
14:            </Editor>
15:        </x:RenderField>
16:        <x:RenderField Width="100px" ColumnID="Gender" DataField="Gender" FieldType="Int"
17:            RendererFunction="renderGender" HeaderText="性别">
18:            <Editor>
19:                <x:DropDownList ID="ddlGender" Required="true" runat="server">
20:                    <x:ListItem Text="男" Value="1" />
21:                    <x:ListItem Text="女" Value="0" />
22:                </x:DropDownList>
23:            </Editor>
24:        </x:RenderField>
25:        <x:RenderField Width="100px" ColumnID="EntranceYear" DataField="EntranceYear" FieldType="Int"
26:            HeaderText="入学年份">
27:            <Editor>
28:                <x:NumberBox ID="tbxEditorEntranceYear" NoDecimal="true" NoNegative="true" MinValue="2000"
29:                    MaxValue="2010" runat="server">
30:                </x:NumberBox>
31:            </Editor>
32:        </x:RenderField>
33:        <x:RenderField Width="100px" ColumnID="EntranceDate" DataField="EntranceDate" FieldType="Date"
34:            Renderer="Date" RendererArgument="yyyy-MM-dd" HeaderText="入学日期">
35:            <Editor>
36:                <x:DatePicker ID="DatePicker1" Required="true" runat="server">
37:                </x:DatePicker>
38:            </Editor>
39:        </x:RenderField>
40:        <x:RenderCheckField Width="100px" ColumnID="AtSchool" DataField="AtSchool" HeaderText="是否在校" />
41:        <x:RenderField Width="100px" ColumnID="Major" DataField="Major" FieldType="String"
42:            ExpandUnusedSpace="true" HeaderText="所学专业">
43:            <Editor>
44:                <x:TextBox ID="tbxEditorMajor" Required="true" runat="server">
45:                </x:TextBox>
46:            </Editor>
47:        </x:RenderField>
48:    </Columns>
49:</x:Grid>

RenderField是专门用于可编辑表格的,我们可以在 RenderField 内部定义 Editor,一个 Editor 也就是一个表单字段。

常用做 Editor 有 TextBox、NumberBox、DropDownList、DatePicker等。

还有一个特殊的列类型是 RenderCheckField,专门用来生成可编辑的复选框,要特别注意 RenderCheckField 和 CheckBoxField 的区别。

为什么用于可编辑表格的列类型都是 Render 开头的呢?

其实这里的 Render 可以理解为客户端渲染,服务器端会把数据准备好,而不会在服务器端生成每个单元格的 HTML(这一点可以和之前的列类型做对比),而是在客户端根据服务器端提供的原始数据渲染成所需要的 HTML。

比如这个例子中的 Gender 列定义了 RendererFunction="renderGender",这里的 renderGender 就是一个 JavaScript 函数:

1:<script>
2:    function renderGender(value, metadata, record, rowIndex, colIndex) {
3:        return value == 1 ? '男' : '女';
4:    }
5:</script>

这里返回的“男”或者“女”就是本列处于非编辑状态下显示的内容,当然我们可以用两个图标分别代表,比如用下面这个函数来替代上面的函数:

1:<script>
2:    function renderGender(value, metadata, record, rowIndex, colIndex) {
3:        return value == 1 ? '<img src="../extjs/res/images/boy.png"/>' : '<img src="../extjs/res/images/girl.png"/>';
4:    }
5:</script>

另一个需要注意的地方,我们为每一列都定义了 ColumnID,这一点很重要。在后台代码中获取用户修改后的数据时,需要用到这个属性。

下面来看下后台如何获取用户的修改值,并保存到持久化设备。

作为示例,我们没有使用数据库,而是在内存中模拟了持久化存储(当然不是真的持久化,也不要在实际项目中这样用):

1: private static readonly string KEY_FOR_DATASOURCE_SESSION = "datatable_for_grid_editor_cell";
2: 
3: // 模拟在服务器端保存数据
4: // 特别注意:在真实的开发环境中,不要在Session放置大量数据,否则会严重影响服务器性能
5: private DataTable GetSourceData() {
6:     if (Session[KEY_FOR_DATASOURCE_SESSION] == null) {
7:         Session[KEY_FOR_DATASOURCE_SESSION] = GetDataTable();
8:     }
9:     return (DataTable) Session[KEY_FOR_DATASOURCE_SESSION];
10: }

在用户点击“保存数据”按钮时,后台处理代码:

1:protected void Button2_Click(object sender, EventArgs e) {
2:    Dictionary<int, Dictionary<string, string>> modifiedDict = Grid1.GetModifiedDict();
3: 
4:    for (int i = 0, count = Grid1.Rows.Count; i < count; i++) {
5:        if (modifiedDict.ContainsKey(i)) {
6:            Dictionary <string, string> rowDict = modifiedDict[i];
7: 
8:            // 更新数据源
9:            DataTable table = GetSourceData();
10: 
11:            DataRow rowData = table.Rows[i];
12: 
13:            // 姓名
14:            if (rowDict.ContainsKey("Name")) {
15:                rowData["Name"] = rowDict["Name"];
16:            }
17:            // 性别
18:            if (rowDict.ContainsKey("Gender")) {
19:                rowData["Gender"] = Convert.ToInt32(rowDict["Gender"]);
20:            }
21:            // 入学年份
22:            if (rowDict.ContainsKey("EntranceYear")) {
23:                rowData["EntranceYear"] = rowDict["EntranceYear"];
24:            }
25:            // 入学日期
26:            if (rowDict.ContainsKey("EntranceDate")) {
27:                rowData["EntranceDate"] = DateTime.Parse(rowDict["EntranceDate"]).ToString("yyyy-MM-dd");
28:            }
29:            // 是否在校
30:            if (rowDict.ContainsKey("AtSchool")) {
31:                rowData["AtSchool"] = Convert.ToBoolean(rowDict["AtSchool"]);
32:            }
33:            // 所学专业
34:            if (rowDict.ContainsKey("Major")) {
35:                rowData["Major"] = rowDict["Major"];
36:            }
37: 
38:        }
39:    }
40: 
41:    labResult.Text = "用户修改的数据:" + Grid1.GetModifiedData().ToString(Newtonsoft.Json.Formatting.None);
42: 
43:    BindGrid();
44: 
45:    Alert.Show("数据保存成功!(表格数据已重新绑定)");
46:}

这里的 GetModifiedDict 函数返回用户在客户端所有的修改数据,它的数据类型是 Dictionary<int, Dictionary<string, string>>,第一个 int 表示行索引,第一个 string 表示列标识(ColumnID),第二个 string 表示用户在客户端修改的值。

理解了这一点,上面的代码就清晰明了了:

1. 首先遍历表格的所有数据行

2. 查看当前数据行是否在客户端修改了?

3. 如果修改了,则查找本行数据中哪些列在客户端修改了,并更新数据源。

是不是对 GetModifiedData 函数感兴趣?

这个函数是服务器接收到的客户端回发的原始数据,是用 JSON 表示的,来看这个例子的结果:

1:[
2:    [2, {
3:        "Name": "董婷婷2",
4:        "Gender": "1",
5:        "EntranceYear": 2009,
6:        "AtSchool": false,
7:        "EntranceDate": "2008-09-02T00:00:00"
8:    }],
9:    [4, {
10:        "EntranceDate": "2008-09-09T00:00:00",
11:        "EntranceYear": 2000
12:    }]
13:]

4.2 可编辑表格的“删”

image

由于只能选中一个单元格,而不是一行数据,所以我们可以通过选中某行单元格来删除本行数据。

1:protected void btnDelete_Click(object sender, EventArgs e) 
2:{
3:    StringBuilder sb = new StringBuilder();
4:    if (Grid1.SelectedCell != null) {
5:        int rowIndex = Grid1.SelectedCell[0];
6: 
7:        GetSourceData().Rows.RemoveAt(rowIndex);
8: 
9:        BindGrid();
10: 
11:        Alert.ShowInTop("删除数据成功!(表格数据已重新绑定)");
12:    } else {
13:        Alert.ShowInTop("没有选中任何单元格!");
14:    }
15: 
16:}

这个过程比较简单,首先获取用户选中的单元格(SelectedCell),这个数组的第一个元素就是行索引,接下来从数据源中删除本行数据,并重新绑定表格即可。

4.3 可编辑表格的“增”

snap248

首先来看下如何为“新增数据”按钮绑定客户端脚本:

1:protected void Page_Load(object sender, EventArgs e) 
2:{
3:    if (!IsPostBack) {
4:        JObject defaultObj = new JObject();
5:        defaultObj.Add("Name", "用户名");
6:        defaultObj.Add("Gender", 1);
7:        defaultObj.Add("EntranceYear", "2015");
8:        defaultObj.Add("EntranceDate", "2015-09-01");
9:        defaultObj.Add("AtSchool", false);
10:        defaultObj.Add("Major", "化学系");
11: 
12:        // 第一行新增一条数据
13:        btnNew.OnClientClick = Grid1.GetAddNewRecordReference(defaultObj, false);
14: 
15:        btnReset.OnClientClick = Grid1.GetRejectChangesReference();
16: 
17:        BindGrid();
18:    }
19:}

GetAddNewRecordReference 函数接受的第一个参数类型是 JObject,用来指定新增数据每一列的默认值,第二个参数指定是否将新增行添加到当前数据的末尾。

如果看下页面源代码,可以发现生成的 JavaScript 如下所示:

1:X('Grid1').x_addNewRecord({
2:    "Name": "用户名",
3:    "Gender": 1,
4:    "EntranceYear": "2015",
5:    "EntranceDate": "2015-09-01",
6:    "AtSchool": false,
7:    "Major": "化学系"
8:}, false);

再来看看保存数据的代码,由于有两部分数据需要保存,一部分是新增的,另一部分是修改现有的数据,所以提取了一个共有函数:

1:private static void UpdateSourceDataRow(Dictionary <string, string> rowDict, DataRow rowData) {
2:    // 姓名
3:    if (rowDict.ContainsKey("Name")) {
4:        rowData["Name"] = rowDict["Name"];
5:    }
6:    // 性别
7:    if (rowDict.ContainsKey("Gender")) {
8:        rowData["Gender"] = Convert.ToInt32(rowDict["Gender"]);
9:    }
10:    // 入学年份
11:    if (rowDict.ContainsKey("EntranceYear")) {
12:        rowData["EntranceYear"] = rowDict["EntranceYear"];
13:    }
14:    // 入学日期
15:    if (rowDict.ContainsKey("EntranceDate")) {
16:        rowData["EntranceDate"] = DateTime.Parse(rowDict["EntranceDate"]).ToString("yyyy-MM-dd");
17:    }
18:    // 是否在校
19:    if (rowDict.ContainsKey("AtSchool")) {
20:        rowData["AtSchool"] = Convert.ToBoolean(rowDict["AtSchool"]);
21:    }
22:    // 所学专业
23:    if (rowDict.ContainsKey("Major")) {
24:        rowData["Major"] = rowDict["Major"];
25:    }
26:}

保存数据的代码则清晰明了:

1: protected void Button2_Click(object sender, EventArgs e) {
2: 
3:     // 1. 先修改的现有数据
4:     Dictionary < int, Dictionary < string, string >> modifiedDict = Grid1.GetModifiedDict();
5:     for (int i = 0, count = Grid1.Rows.Count; i < count; i++) {
6:         if (modifiedDict.ContainsKey(i)) {
7:             Dictionary < string, string > rowDict = modifiedDict[i];
8: 
9:             // 更新数据源
10:             DataTable table = GetSourceData();
11: 
12:             DataRow rowData = table.Rows[i];
13: 
14:             UpdateSourceDataRow(rowDict, rowData);
15: 
16:         }
17:     }
18: 
19: 
20:     // 2. 再新增数据
21:     List < Dictionary < string, string >> newAddedList = Grid1.GetNewAddedList();
22:     for (int i = newAddedList.Count - 1; i >= 0; i--) {
23:         DataTable table = GetSourceData();
24: 
25:         DataRow rowData = table.NewRow();
26: 
27:         UpdateSourceDataRow(newAddedList[i], rowData);
28: 
29:         table.Rows.InsertAt(rowData, 0);
30:     }
31: 
32: 
33:     labResult.Text = "用户修改的数据:" + Grid1.GetModifiedData().ToString(Newtonsoft.Json.Formatting.None);
34: 
35:     BindGrid();
36: 
37:     Alert.Show("数据保存成功!(表格数据已重新绑定)");
38: }

我们可以看到,修改现有数据的代码和之前的一模一样,都是先使用 GetModifiedDict 获取用户在客户端修改的值。

保存新增数据行的代码更加简单:

1. 使用 GetNewAddedList 方法返回新增的数据行列表;

2. 遍历每一行,将新增数据添加到数据源中。

需要注意:

1. 一定要修改现有数据,然后再处理新增数据

2. 处理完后一定要重新绑定数据,因为此时前段显示和后端的数据已经不一致了。

5. 顶部菜单框架

这个需求也是来源于论坛用户。官网示例给出的是左侧菜单结构的框架,那么如何实现顶部菜单结构的框架呢?

snap249

如图所示,点击顶部菜单来更新左侧树结构,实现起来倒不难,不过需要一点 JavaScript 知识。

首先来看下顶部菜单的定义:

1:<ul>
2:    <li class="selected menu-mail">
3:        <asp:LinkButton ID="lbtnMail" runat="server" OnClick="lbtnMail_Click">
4:            <span>邮件收发</span></asp:LinkButton>
5:    </li>
6:    <li class="menu-sms">
7:        <asp:LinkButton ID="lbtnSMS" runat="server" OnClick="lbtnSMS_Click">
8:            <span>短信收发</span></asp:LinkButton>
9:    </li>
10:    <li class="menu-sys">
11:        <asp:LinkButton ID="lbtnSYS" runat="server" OnClick="lbtnSYS_Click">
12:            <span>系统管理</span></asp:LinkButton>
13:    </li>
14:</ul>

后台代码中,分别处理三个顶部菜单的点击事件,更新左侧树控件即可:

1:private void BindLeftTree(string menuType) {
2:    if (menuType == "mail") {
3:        XmlDataSource1.DataFile = "./data/menuMail.xml";
4:        PageContext.RegisterStartupScript("selectMenu('menu-mail');");
5:    } else if (menuType == "sys") {
6:        XmlDataSource1.DataFile = "./data/menuSYS.xml";
7:        PageContext.RegisterStartupScript("selectMenu('menu-sys');");
8:    } else if (menuType == "sms") {
9:        XmlDataSource1.DataFile = "./data/menusms.xml";
10:        PageContext.RegisterStartupScript("selectMenu('menu-sms');");
11:    }
12: 
13:    BindLeftTree();
14:}
15: 
16:private void BindLeftTree() {
17:    leftTree.DataSource = XmlDataSource1;
18:    leftTree.DataBind();
19:}
20: 
21:protected void lbtnMail_Click(object sender, EventArgs e) {
22:    BindLeftTree("mail");
23:}
24:protected void lbtnSYS_Click(object sender, EventArgs e) {
25:    BindLeftTree("sys");
26: 
27:}
28:protected void lbtnSMS_Click(object sender, EventArgs e) {
29:    BindLeftTree("sms");
30:}

但是不要忘了在切换顶部菜单时,更新选中菜单的样式,同时要选中树控件的第一个节点,并在主区域内加载此节点所指向的页面:

1:<script>
2:    var leftTreeID = '<%= leftTree.ClientID %>';
3: 
4:    function selectMenu(menuClassName) {
5:        // 选中当前菜单
6:        Ext.select('.menu ul li').removeClass('selected');
7:        Ext.select('.menu ul li.' + menuClassName).addClass('selected');
8: 
9:        // 展开树的第一个节点,并选中第一个节点下的第一个子节点(在右侧IFrame中打开)
10:        var tree = X(leftTreeID);
11:        var treeFirstChild = tree.getRootNode().firstChild;
12:        // 展开第一个节点(如果想要展开全部节点,调用 tree.expandAll();)
13:        treeFirstChild.expand();
14: 
15: 
16:        // 选中第一个链接节点,并在右侧IFrame中打开此链接
17:        var treeFirstLink = treeFirstChild.firstChild;
18:        treeFirstLink.select();
19:        window.frames['mainframe'].location.href = treeFirstLink.attributes['href'];
20: 
21:    }
22: 
23:    function onReady() {
24:        selectMenu('menu-mail');
25:    }
26:</script>

虽然写了一点 JavaScript 代码,但最终还是实现了我们需要的结果。

不过想要实现如下界面,就不那么容易了:

snap250

你可能会想,不就是把树控件换成手风琴控件么,不和上例一样的么?

如果你真的这么想,那你就需要先了解下 ASP.NET 下动态创建控件的游戏了,先看这篇文章:http://www.cnblogs.com/sanshi/archive/2012/11/19/2776672.html



原因是树控件是一个控件,可以通过更新数据源来重新加载;而手风琴控件是由多个控件组合而成的,无法在页面回发时重新创建另一个手风琴控件!

怎么办呢?

办法总会有的,我们可以把左侧的区域也做成 IFrame,这样每次点击顶部菜单时,就重新加载左侧 IFrame(动态创建手风琴控件)就行了(是不是很妙眨眼)!

这里只提供一个思路,具体的例子请查看:http://fineui.com/demo/#/demo/iframe/topmenu3/default.aspx

下载 FineUI v3.3.0 和官方示例

下载地址:http://fineui.codeplex.com/releases/

FineUI严格遵守 ExtJS 关于开源软件的规则,不再内置 ExtJS 库。 

获取适用于 FineUI 的 ExtJS 库:http://fineui.com/bbs/forum.php?mod=viewthread&tid=3218 

基于 FineUI 的空项目(Net2.0 和 Net4.0 两个版本):http://fineui.com/bbs/forum.php?mod=viewthread&tid=2123

FineUI大版本升级,外置ExtJS库、去AXD化、表格合计行、表格可编辑单元格的增删改、顶部菜单框架相关推荐

  1. FineUI大版本升级,外置ExtJS库、去AXD化、表格合计行、表格可编辑单元格的增删改、顶部菜单框架...

    FineUI v3.3.0 更新的内容非常多,所以一下子从 v3.2.6 连跳 3 个小版本,直接来到了 v3.3.0.详细的更新记录请参考这里:http://fineui.com/version 主 ...

  2. FineUI大版本升级,外置ExtJS库、去AXD化、表格合计行、表格可编辑单元格的增删改、顶......

    2019独角兽企业重金招聘Python工程师标准>>> FineUI v3.3.0 更新的内容非常多,所以一下子从 v3.2.6 连跳 3 个小版本,直接来到了 v3.3.0.详细的 ...

  3. ExtJS EditorGridPanel 示例之Array格式(自定义Array解析器)Store前后台增删改查

    本示例入口html页面: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http: ...

  4. TokenGazerOKex Research | 去中心化云存储行业报告

    2019年5月6日,TokenGazer与OKex联合发布了去中心化云存储行业报告.当前TokenGazer往期的项目评级报告.深度研究报告.加密货币月报等均已收录在官方网站:www.tokengaz ...

  5. 区块链的去中心化VS传统互联网的去中心化:技术与治理的双重困境

    链客,专为开发者而生,有问必答! 此文章来自区块链技术社区,未经允许拒绝转载. 区块链的去中心化VS传统互联网的去中心化:技术与治理的双重困境11 主要观点: 1.传统互联网经典的去中心化项目BitT ...

  6. 杂记 去中心化系统介绍

    一.去中心化系统概述 去中心化系统(Decentralized System)是一类没有任何中央协调或管理单元的系统.换句话说,没有一个单一的中央服务器来协调或管理系统.与集中式系统相比,分散式系统既 ...

  7. 闪电网络如何实现更加去中心化的网络

    启用点对点金融 与其他支付技术相比,闪电网络的定义特征之一是其点对点架构.虽然重要的是要承认并不是每个人都会实际运行自己的闪电节点,但设置和操作一个供个人使用的节点已经相对简单明了,我们可以期待运营闪 ...

  8. 去中心化云存储技术 | CESS 的多层网络架构详解

    区块链以其特有的分布式算法和技术底层,让链上数据也随着区块链的不同实现了分布式存储,CESS(Cumulus Encrypted Storage System)去中心化云储存网络基础设施,让我们看到了 ...

  9. 《这就是区块链》之区块链基础(5)--去中心化的意义

     在上一篇中,花了比较大的篇幅来阐述去中心化的概念.其实对于有计算机基础的朋友来讲,去中心化或者P2P网络并不是新颖的观点.但是,我还是希望本系列文章可以从基础展开,让所有的朋友都可以理解区块链的 ...

最新文章

  1. 在64位机上PLSQL连oracle11g问题:SQL*Net not properly installed和ORA-12154:TNS:无法处理服务名...
  2. json 字符串传到action之后的处理,遍历. 练习代码片
  3. linux 内存管理 page fault带来的性能问题
  4. 不能解决,复选框在request对象获取的信息后显示在用户信息里面为中文的选项名...
  5. Hexo瞎折腾系列(8) - 添加评论系统
  6. lg gram 笔记本 linux,lg gram 15笔记本使用雨林木风u盘安装win7系统教程?
  7. 室内空气流动原理图_家庭新风系统示意图 新风系统运行原理介绍
  8. Cpp / 拷贝构造函数的参数为什么必须使用引用类型
  9. Hibernate基于JDBC的批量删除
  10. oracle数据库集群采用的是形式,铁道部采用Oracle集群数据库进行TMIS系统“三级建库”...
  11. mybatis-generator 根据表生成对应文件
  12. # heapsort
  13. ubuntu 导入mysql_Ubuntu16.04系统mysql命令导入导出sql文件
  14. halcon深度学习
  15. GEE行政区加载,高程坡度计算和裁剪
  16. 什么是SSL数字证书
  17. 基于STM32风速风向检测仿真
  18. Linux学习笔记——系统函数IO
  19. 玩这么久 Python ,这些好玩又实用的库一定不能错过!
  20. java软件如何优雅地写配置文件

热门文章

  1. 冯诺依曼原理计算机称为,冯·诺依曼提出的计算机工作原理又称为 工作原理。...
  2. 数据结构查找-7-7 词典 (15 分)
  3. 关于技术人员创业的几点建议
  4. 微服务商城系统(十) Spring Security Oauth2 + JWT 用户认证
  5. 三玖天下第一!让三玖陪你写代码(为VScode加入背景图片)
  6. 计算机系统中必不可少的系统软件是哪个,计算机系统中必不可少的软件
  7. 零基础使用Swift学习数据科学
  8. 蓄电池内阻的检测技术
  9. mac浏览器打不开java_苹果电脑Mac打不开网页怎么办?Mac打不开网页的解决方法...
  10. rust 使用tokio的Notify 和timeout实现类似可超时条件变量的效果