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

FineUI v3.3.0 更新的内容很是多,因此一会儿从 v3.2.6 连跳 3 个小版本,直接来到了 v3.3.0。详细的更新记录请参考这里:http://fineui.com/versionphp

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

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

 

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

 

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 的源代码!app

 

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