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

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 the GNU 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 ,别忘了点击页面右下角的“推荐”按钮哦!

    posted @ 2013-06-17 08:17  三生石上(FineUI控件)  阅读(17820)  评论(49编辑  收藏  举报