65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 MVC 模式的 JavaScript DataGrid

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.81/5 (24投票s)

2007 年 5 月 14 日

CPOL

7分钟阅读

viewsIcon

160315

downloadIcon

2343

此网格支持调整、拖动和排序列。它使用 MVC 模式实现。

Screenshot - gridScreen.jpg

引言

MVC 模式是将应用程序或 GUI 控件分解为三部分的方式:控制器、模型和视图。MVC 模式旨在处理输入、数据处理和将数据渲染到 GUI 领域。用户输入、数据处理和数据渲染由视图、模型和控制器对象分离并处理。

控制器解释用户发出的鼠标和键盘输入,并将这些命令映射到视图和模型对象以改变状态。模型管理一个或多个数据元素。视图管理显示器的客户端区域,并负责向用户呈现数据。视图通常有一套知道如何渲染图形的方法包。控制器是一个负责用户与应用程序交互的对象。控制器接受用户输入并强制视图和模型执行操作。例如,如果用户单击鼠标按钮或单击界面上的某个项目,控制器负责确定应用程序应如何响应最终用户。模型、视图和控制器是相关的。因此,它们必须相互引用(下图显示了模型-视图-控制器关系)。

我认为 MVC 范式是分解 GUI 控件的最佳方式,这就是我选择这种模式的原因,而且我确实喜欢这种模式。无论您使用什么环境,无论是 Java、C# 还是 JavaScript,这都无关紧要。其思想是将应用程序分解为三个逻辑类。

                 --------------
                 |   Model    |
                 --------------
                /\           /\
                /             \
               /               \
              /                 \
             /                   \
      -------------- <------ --------------
      |    View    |         | Controller |
      --------------         --------------

背景

我的热情是 AJAX 应用程序和 JavaScript 控件。我不喜欢在浏览器中使用 ActiveX 或 Java 小程序。当然,有很多原因您必须这样做,但在大多数情况下,我认为通过 JavaScript 代码实现会更好。我指的是 GUI 控件。

代码

如上所述,MVC 范式假定将应用程序或 GUI 控件分解为三个逻辑类。此控件中有三个逻辑类。

var dataSource = new DataSource();//implements the model
var view = new View(idHolder,columnsWSize,dataSource);
var controller = new Controller(view, dataSource);

controller 类处理所有事件并强制 dataSourceview 类执行某些操作。

this.onRemoveRow = function(rowId)
{ 
    view.removeRow(rowId);
    dataSource.removeRow(rowId);
}
this.onUpdateRow = function(rowID)
{
    view.setGridUnselectedState();
    view.setRowWriteState(rowID);
}
this.onAddRow = function()
{ 
    view.setGridUnselectedState();
    view.setRowAddState();
}
view.dataClickedEventHandler = function()
{
    thisRef.hideAllMenu();
    view.setGridUnselectedState();
    event.cancelBubble = true; 
}
....

dataSource 类有一套处理数据的方法。

var dataHeaders = new Array();
var dataRows = new Array();
var sortDirection= asc;
var sortIndex = 0; 

function sortItems(data1,data2)
{
    var data1Value = data1[sortIndex];
    var data2Value = data2[sortIndex];
          
    if(data1Value==null)
        data1Value = "";
            
    if(data2Value==null)
        data2Value = "";
    data1Value = data1Value.toUpperCase();
    data2Value = data2Value.toUpperCase();
     
    if(sortDirection==asc)
    {
        if(data1Value < data2Value)
            return -1;
        else 
        if(data1Value > data2Value)
            return 1;
        else 
            return 0;      
    }
    else 
    {
        if(data1Value > data2Value)
            return -1;
        else 
            if(data1Value < data2Value)
                return 1;
            else 
                return 0;   
    }
}
this.clear = function()
{
    dataHeaders = new Array();
    dataRows = new Array();
    sortDirection= asc;
    sortIndex = 0; 
}
...

view 类有一套处理 GUI 的方法。

this.isRowWriteState = function(rowId)
{
    for(var index=0;index < dataContent.rows.length;index++)
    {  
        if(dataContent.rows[index].cells
            [dataContent.rows[index].cells.length-1].firstChild.children
            [1]!=null && dataContent.rows[index].rowId == rowId)
                return true;
    }
}
this.setRowSelectedState  = function(rowId)
{ 
    for(var index=0; index < dataContent.rows.length; index++)
    {
        if(dataContent.rows[index].rowId==rowId)
        {   
            for(var index1 = 0; index1 < 
                dataContent.rows[index].cells.length; index1++)
            {   
                var width = dataContent.rows[index].cells[index1].style.width;
                dataContent.rows[index].cells[index1].style.cssText = 
                    styleCellSelectedData;
                dataContent.rows[index].cells[index1].style.width = width;
            }
            break;
        }
             
    }
    this.selectedRowId = rowId;
}
this.setGridReadState = function()
{
    for(var index=0;index < dataContent.rows.length;index++)
    {  
        //if the stub column has buttons that means the row is in write state
        if(dataContent.rows[index].cells[dataContent.rows
            [index].cells.length-1].firstChild.children[1]!=null)
            {
                this.setRowReadState(dataContent.rows[index].rowId);
            }
    }
}
this.setGridNotAddState = function()
{  
if(data.children.length==2)
    data.children[0].parentElement.removeChild(data.children[0]);
}
...

缺点

此网格有一些缺点。它只支持 Internet Explorer(我没有太多时间为它编写额外的代码使其成为跨浏览器控件)。但这是一个次要的缺点;主要缺点是代码执行。在调整列大小和创建期间,执行效果不是很好。由于它涉及单元格中文本的截断,我编写了执行截断的实用方法。

function junkTextToWSize(str , wSize , cssText)
{
    var textLabel = document.getElementById("uxTextLabel");
    if(textLabel==null)
    {
        textLabel = document.createElement("");
        textLabel.id = "uxTextLabel"; 
        document.body.appendChild(textLabel);
    }
    textLabel.innerText = str;
    textLabel.style.cssText = cssText;
    textLabel.style.visibility = "hidden";
    textLabel.style.position = "absolute"; 
          
    if(textLabel.offsetWidth>=wSize)
    {
        textLabel.innerText = "..."+textLabel.innerText
        while(textLabel.offsetWidth>=wSize)
        {
            textLabel.innerText = textLabel.innerText.substr
                        (0,textLabel.innerText.length-1);
        }
        var retString = textLabel.innerText.substr
            (3,textLabel.innerText.length-3)+"...";
        return retString;
    }

    else 
    return str;
}

此方法创建一个临时 span 元素来检查文本的大小。如果文本标签太长,它必须被截断并带有后缀“...”。对于 Internet Explorer 来说,调用 offsetWidth 太昂贵了。我编写了一个简单的函数来尝试调用 offsetWidth

function buildTable()
{       
      var table = document.all.uxTest.appendChild
          (document.createElement("<table>"));
      for(var index=0; index<401; index++ ) 
      {
         var row = table.insertRow();
         for(var index1=0; index1 < 11; index1++)
         {           
              var cell = row.insertCell()
              cell.innerText = index+"string"+index1;
              // var offsetWidth = cell.offsetWidth;             

         }        
      }   
}
buildTable();

此调用大约需要 6 秒,但如果您取消注释“var offsetWidth = cell.offsetWidth;”,则需要超过 1 分钟才能执行。这就是我将网格中的行数限制为 300 行的原因。我仍然对如何在不调用 offsetwidth 属性的情况下计算文本宽度感兴趣。

网格功能

此网格支持拖动列、调整列大小和排序列。双击列标题将导致网格按单击的标题列排序。要调整列大小,您需要按住鼠标按钮移动标题分隔符。如果文本标签太长而无法放入单元格中,它将导致调整大小列中的文本标签被截断。要拖动列,您需要按住鼠标按钮将鼠标移动到标题上。

网格还有两种菜单:选定行菜单和网格菜单。右键单击行将导致出现选定行菜单,右键单击行以外的任何地方将导致出现网格菜单。网格有两个事件:“onRowUpdated”和“onRowAdded”。这些事件在单击“Add”、“Update”按钮时触发,并返回包含网格行新值的 obj

Screenshot - addUpdateScreen.jpg

要将行设置为“add”、“update”状态,您需要调用网格方法“grid.addRow()”、“grid.updateRow(rowId)”。

Using the Code

在任何 Web 应用程序中使用它都非常简单。

var grid = new GolikGrid(null,
 [150,30,150,100,150,150,150,150,150,150],500,900 ,"uxGridHolder");
var dataSource = grid.getDataSource();
dataSource.addHeaderItem("Company Name");
dataSource.addHeaderItem("Contact Name");
dataSource.addHeaderItem("Company Title");
dataSource.addHeaderItem("Address");
dataSource.addHeaderItem("City");
dataSource.addHeaderItem("Region");
dataSource.addHeaderItem("Postal Code");
dataSource.addHeaderItem("Country");
dataSource.addHeaderItem("Phone");
dataSource.addHeaderItem("Fax");
       
dataSource.addRowItem(1,new Array("Wilman Kala","Serch",
    "Owner/Marketing Assistant","Adenauerallee 900",
    "Seattle","SP","51100","Germany","0241-039123","089-0877451"));
dataSource.addRowItem(2,new Array("Wilman Kala","Serch",
    "Owner/Marketing Assistant","Adenauerallee 900",
    "Seattle","SP","51100","Germany","0241-039123","089-0877451"));
dataSource.addRowItem(3,new Array("Wilman Kala","Serch",
    "Owner/Marketing Assistant","Adenauerallee 900",
    "Seattle","SP","51100","Germany","0241-039123","089-0877451"));
dataSource.addRowItem(4,new Array("HILARION-Abastos","Dima",
    "Sales Representative","Luisenstr. 48","Sao Paulo",
    "WA","8200","Denmark","(5) 555-4729","(171) 555-9199"));
grid.dataBind();
    
var grid = new GolikGid(param1,param2,param3,param4,param5);
  • Param1 - 网格的 ID,可以省略,并可以传递 null 而不是实际 ID。
    注意:如果您只使用一个网格,可以忽略此参数并传递 null,但如果您要使用多个网格,则必须为每个网格传递唯一的 ID。
  • Param2 – 列宽度的数组。
    注意:最好将列设置为文本的实际宽度,否则它必须调用“junkTextToWSize”方法,这会导致过载。
  • Param3 - 网格的高度
  • Param4 - 网格的宽度
  • Param5 – 网格的 ID 保持器
    注意:如果省略参数并传递 null,则网格将被添加到 body 元素而不是指定的元素。
dataSource.addRowItem(param1,param2);
  • Param1 - 行的 ID
  • Param2 – 单元格数组

初始化菜单

如前所述,网格中有两种菜单:项目菜单和网格菜单。网格菜单在行以外的任何地方(在滚动条、标题、应该有行的空白区域)右键单击时出现,行菜单在行上右键单击时出现。构建您自己的菜单非常简单。您需要做的就是创建一个对象,其中“key”将被视为菜单项名称,“value”将被视为菜单项处理程序,然后将其传递给相应的方法。

var rowMenuItems = {"Add New":onMenuAddNew,"Update":onMenuUpdateRow,
    "Remove":onMenuRemoveRow,"Refresh":onMenuRefresh};
var gridMenuItems = {"Refresh":onMenuRefresh,
    "Remove Column":onMenuRemoveColumn,
    "Add New Row":onMenuAddNew,"Help":onMenuHelp}; 
grid.setItemMenu(rowMenuItems);
grid.setGridMenu(gridMenuItems);

您还需要实现菜单项的所有已声明处理程序。例如,“Help”网格菜单项处理程序

function onMenuHelp()
{
    window.open("about:blank");
}

在网格中添加、更新和删除行

网格没有将行设置为“update”、“add”状态的本机事件,但当这些操作完成时会触发两个事件。

//assign the event handler for onRowUpdated and onRowAdded event
grid.onRowUpdated = onGridRowUpdated;
grid.onRowAdded = onGridRowAdded;

要将行设置为更新状态,您需要在某个地方调用“grid.updateRow(rowId)”。例如,“Update”行菜单项处理程序。

function onMenuUpdateRow()
{
    grid.updateRow(grid.getSelectedRowId());
}

由于“onMenuUpdateRow”是行菜单的处理程序,因此可以通过调用“grid.getSelectedRowId()”方法获取“selected RowId”。此调用将选定的行设置为更新状态。用户可以通过单击取消按钮来取消更新。一旦用户单击“更新”按钮,网格就会触发事件“grid.onRowUpdated”。
例如,“onGridRowUpdated”事件处理程序。

function onGridRowUpdated(row)
{
    var updateResult = "The grid row has been updated.\n";
    updateResult+=   "ID "+row.id+"\n";
    updateResult+=   "Company Name:  "+row
        ["Company Name"]+"\n";
    updateResult+=   "Contact Name:  "+row
        ["Contact Name"]+"\n";
    updateResult+=   "Company Title:  "+row
        ["Company Title"]+"\n";
    updateResult+=   "Address:  "+row
        ["Address"]+"\n";
    updateResult+=   "City: "+row["City"]+"\n";
    updateResult+=   "Region: "+row
        ["Region"]+"\n";
    updateResult+=   "Postal Code: "+row
        ["Postal Code"]+"\n";
    updateResult+=   "Country: "+row
        ["Country"]+"\n";
    updateResult+=   "Phone: "+row
        ["Phone"]+"\n";
    updateResult+=   "Fax: "+row["Fax"]+"\n";  
    alert(updateResult);
    customDataSource.updateRow(row.id,new Array(
        row["Company Name"] ,row["Contact Name"],
        row["Company Title"],row["Address"],
        row["City"],row["Region"],
        row["Postal Code"],row["Country"],
        row["Phone"],row["Fax"]));
}

此事件处理程序有一个参数,其中包含更新行的数据。每个值都可以通过标题名称访问,然后保存到某个地方(在当前示例中,它是一个自定义的 datasource 对象,在页面刷新之前作为持久存储实现)。

添加新”行菜单项处理程序也是如此。

function onMenuAddNew()
{
    grid.addRow();
}
function onGridRowAdded(row)
{
    var updateResult = "The grid row has been added.\n";
    updateResult+=   "Company Name:  "+row
       ["Company Name"]+"\n";
    updateResult+=   "Contact Name:  "+row
       ["Contact Name"]+"\n";
    updateResult+=   "Company Title:  "+row
       ["Company Title"]+"\n";
    updateResult+=   "Address:  "+row
       ["Address"]+"\n";
    updateResult+=   "City: "+row
       ["City"]+"\n";
    updateResult+=   "Region: "+row
       ["Region"]+"\n";
    updateResult+=   "Postal Code: "+row
       ["Postal Code"]+"\n";
    updateResult+=   "Country: "+row
       ["Country"]+"\n";
    updateResult+=   "Phone: "+row
       ["Phone"]+"\n";
    updateResult+=   "Fax: "+row
       ["Fax"]+"\n";  
    customDataSource.addRowItem(new Array(row["Company Name"],
       row["Contact Name"],row["Company Title"],
       row["Address"],row["City"],
       row["Region"],row["Postal Code"],
       row["Country"],row["Phone"],row["Fax"]));
    
    if(window.confirm("The row has been successfully added, 
        refresh the grid?"))
        initGrid();
}

onMenuAddNew”行菜单项处理程序不需要行 ID,因此此行菜单事件处理程序可以添加到网格菜单。添加新行时,您必须为此行分配新的唯一 ID,因此您需要将其放入持久存储中,然后重新初始化网格以查看新项。

onMenuRemoveRow”行菜单项处理程序不能添加到网格菜单,因为它需要只有在行菜单项上下文中才能访问的删除行的 ID。

function onMenuRemoveRow()
{
    var selectedId = grid.getSelectedRowId();
    if(window.confirm("Are sure you want to remove this item?"))
    { 
        grid.removeRow(selectedId);
        customDataSource.removeRow(selectedId);
     }
}

这种方法提供了一种灵活的方式来构建您自己的菜单,但它不一定只能构建菜单。您可以通过其他方式对网格进行任何修改。例如,它可能是一个带有“删除、更新、添加、刷新”图标的漂亮工具栏。

请不要在您的项目中原样使用此网格。它只是一个处理数据的网格演示版本,需要进行良好的测试。

如果您觉得这篇文章有趣,请为我投票。

© . All rights reserved.