博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

[翻译]asp.net 1.x/2.0中的高级列表控件

Posted on 2006-12-13 21:54  webabcd  阅读(4628)  评论(2编辑  收藏
原文地址:http://aspalliance.com/1071_Smart_ListControl_in_ASPNET_1x20
[原文源码下载]
[译者改后源码下载]


[翻译]asp.net 1.x/2.0中的高级列表控件


原文发布日期:2006.11.16
作者:Bilal Haidar
翻译:webabcd


摘要
本文中Bilal Haidar将告诉你如何在asp.net 1.x/2.0下开发一个高级列表控件,使它可以在客户端保存其自身每一项的变化。


文章内容
介绍
解决方案
类的实现
实现IPostBackDataHandler接口
如何使用新控件
下载
结论


介绍
在 asp.net 中,当aspx页上的一个列表控件发生回发事件到服务端时,这个列表控件的功能如下。

无论是依靠WebForm中的Forms还是QueryString方式传值,列表控件中的各个项都不会回发至服务端。

只有列表控件中被选中的值才会在一个回发事件里把值传递到服务端,这说明一个被选中的项可以在服务端被侦测到。

当这个响应返回到客户端时,列表控件中的各个项会从视图状态检索到。

以上这个事实暴露出了一些列问题,特别是当你在客户端作一些变化的时候。比如,在列表控件中删除或新增项。伴随着页面回发至服务端,列表控件在客户端所做的这些改变都不会被服务端侦测到。无论你做过什么改变,再一次回发之后,都会消失不见,列表控件又会重新从视图状态中加载各个项。

在一些程序中会有对列表控件添加项和删除项的要求。想象一下,在页面上有两个列表控件,然后又一些按钮可以让两个列表控件互相移动各自的项。举个例子,有两个ListBox分别放置已完成和未完成的任务,当一个任务完成时,你可以把这个任务从未完成的ListBox中转移到已完成的ListBox中,但是稍后你会发现,你没有把那个已完成的任务从未完成的ListBox转移出来。

为了让你的webform有一个更好的表现,比如像实现上面所说的功能,我们需要用一些客户端的javascript来代替某些服务端的操作。但是,作为一个列表控件,ListBox将会有无法保存其各个项变化的问题,因为这是在asp.net里ListBox和任何其他的列表控件的默认的行为。

本文的用意就是解决这个问题,使列表控件的各个项的变化可以被保存,可以被服务端侦测到。

我们用ListBox控件做例子,在aps.net里的其他列表控件也都可以使用同样的技术使其转化为高级列表控件。


解决方案
这部分我们将在一个实际的方案中去解决问题。我们需要在新的控件中加入两个隐藏字段。

第一个隐藏字段的命名规则如下:
ListBoxID + "_ADDED"
上面这个字段将用来保存ListBox控件的增加项。
假设要增加项:
Text = "First Item"
Value = "1"
一个新项将被增加到这个隐藏字段中:
Text|Vale
例,上面的项增加后该隐藏字段则增加:
First Item|1
任意增加项都将被加到这个隐藏字段中
Item1|Value1,Item2|Value2
每一项以何种格式添加到隐藏字段中是有非常严格的要求的。正像你从上面看到的那样,每一项都要用“,”分隔开,如此就能把每一个单独的项作为一个新的ListItem添加到ListBox控件中。

第二个隐藏字段的命名规则如下:
ListBoxID + "_REMOVED"
上面这个字段将用来保存ListBox控件的删除项的value。稍后我们将在服务端看到ListBox控件被选中的值和被删除的值,从而反应出客户端的行为和变化。

除了上面所提及的解决方案的一部分,我们还将实现IPostBackDataHandler接口,这个接口有两个方法,LoadPostData和RaisePostDataChangedEvent。本文将对这个接口的实现做简要论述。


类的实现
当我们要改善ListBox控件之后,首先要做的就是要继承ListBox类。目标是给这个类增加实现客户端各个项变化的功能。

下面的代码说明了如何继承ListBox类。

列表1

public class xListBox : ListBox
{
}

ListBox控件所有的公共属性和方法都将可以像以前一样被使用。有一个要引起注意的方法,就是OnPreRender,这个方法将被override以增加前面所说的两个隐藏字段。

OnPreRender如下:

列表2
protected override void OnPreRender(EventArgs e)
{
  
base.OnPreRender(e);
 
  
// 将控件注册为要求在页回发至服务器时进行回发处理的控件。
  if (Page != null)
    Page.RegisterRequiresPostBack(
this);
 
  
// 注册保存增加项的隐藏字段
  Page.RegisterHiddenField(this.HFItemsAdded, "");
 
  
// 注册保存删除项的隐藏字段
  Page.RegisterHiddenField(this.HFItemsRemoved, "");
 
  
// 注册包含在UtilMethods里的一段客户端javascript
  if (!Page.IsClientScriptBlockRegistered("UtilMethods"))
    Page.RegisterClientScriptBlock(
"UtilMethods", jsScript);
}

当你要写自己自定义控件的时候,几乎每次都要override基类的OnPreRender,这是通过基类增加功能,保存代码的通常做法。

任何客户端代码,像javascript或者任何注册在父控件的控件,都应该在控件的这个生命周期(PreRender)中实现。

注意,我们已经在关键字UtilMethods注册了一段javascript代码。这段代码提供了两个客户端函数,主要是AddItemToList和RemoveItemFromList。这些函数使用了前面所提到的两个隐藏字段。它们已经实现了这样一个功能,就是两个隐藏字段保持同步,以使两个隐藏字段不会出现同一项。

这段javascript代码出示如下:

列表3
// 增加新项到 ListBoxID_ADDED 隐藏字段中
function AddListItem(listName, text, value)
{
  
var hiddenField = GetHiddenField(listName.id + '_ADDED');
 
  
if (hiddenField != null)
  
{
    
// 加一个分隔符
    var tmp = hiddenField.value;
    
if (tmp != '')
      hiddenField.value 
+= ',';
 
    
// 加一项到隐藏字段
    hiddenField.value += text + '|+ value;
 
    
// 如果删除字段中有这项则在删除字段中删之
    var removeHiddenField = GetHiddenField(listName.id + '_REMOVED');
    
if (removeHiddenField != null)
    
{
      
var removedItems = removeHiddenField.value.split(',');
      removeHiddenField.value 
= '';
      
for (var i = 0; i < removedItems.length; i++)
      
{
        
if (value != removedItems[i])
        
{
          
// 加一个分隔符
          var tmp1 = removeHiddenField.value;
          
if (tmp1 != '')
            removeHiddenField.value 
+= ',';
 
          removeHiddenField.value 
+= removedItems[i];
        }

      }

    }

  }

}

 
// 将删除项的value增加到 ListBoxID_REMOVED 隐藏字段中
function RemoveListItem(listName, value)
{
  
var hiddenField = GetHiddenField(listName.id + '_REMOVED');
 
  
if (hiddenField != null)
  
{
    
// 加一个分隔符
    var tmp = hiddenField.value;
    
if (tmp != '')
      hiddenField.value 
+= ',';
 
    hiddenField.value 
+= value;
 
    
// 如果增加字段中有这项则在增加字段中删之
    var addHiddenField = GetHiddenField(listName.id + '_ADDED');
    
if (addHiddenField != null)
    
{
      
var addedItems = addHiddenField.value.split(',');
      addHiddenField.value 
= '';
      
for (var i = 0; i < addedItems.length; i++)
      
{
        
if (addedItems[i].match(value) == null)
        
{
          
// 加一个分隔符
          var tmp1 = addHiddenField.value;
          
if (tmp1 != '')
            addHiddenField.value 
+= ',';
 
          addHiddenField.value 
+= addedItems[i];
        }

      }

    }

  }

}

 
// 在页中找到某个隐藏字段
function GetHiddenField(fieldName)
{
  
var hiddenField;
  hiddenField 
= document.getElementById(fieldName);
 
  
if (hiddenField != null)
    
return hiddenField;
 
  
return null;
}
 

上面这段代码已经写到了OnPreRender方法中,稍后当我们讨论“如何使用新控件”的时候再和大家说明如何调用这些方法。

这有三个方法:
AddListItem
RemoveListItem
GetHiddenField

AddListItemt通过取得用于保存增加项的隐藏字段的ID设置该隐藏字段所包含的ListBox控件的增加项。新增加的项将会按照(Text|Value)的格式增加到用于保存增加项的隐藏字段中。如果其中有的项在用于保存删除项的隐藏字段中,则在用于保存增加项的隐藏字段中删之。这保证了删除项保存在用于保存删除项的隐藏字段,增加项保存在用于保存增加项的隐藏字段的正确性。

RemoveListItem通过取得用于保存删除项的隐藏字段的ID设置该隐藏字段所包含的ListBox控件的删除项。被删除的项的value将会保存到用于保存删除项的隐藏字段中,每一项用分隔符“,”标记。每次删除都会检查一次用于保存增加项的隐藏字段以保证用于保存删除项的隐藏字段和用于保存增加项的隐藏字段的值不会有冲突。

最后的GetHiddenField是一个在网页中找到隐藏字段的常用方法。


实现IPostBackDataHandler接口
IPostBackDataHandler接口包含两个主要方法:
IPostBackDataHandler.RaisePostDataChangedEvent
IPostBackDataHandler.LoadPostData

当你继承了一个控件后怎么实现这两个方法呢?

虽然这个问题不是本文的重点,但我们仍然会简要的介绍一下继承自这个接口的控件如何实现这两个方法。

当一个回发事件发生时,页面将重新创建控件。最开始创建的是页面的主控件,然后是其子控件,再然后是子控件的子控件……

每个控件都将被检查是否实现了某些接口。IPostBackDataHandler接口就是其中之一。如果这个接口被实现了,就意味着上面所提及的两个方法被实现了。

这些方法通常被实现去处理每一次的回发数据。默认的,aps.net中的ListBox控件将实现以上的两个方法。

LostPostData实现的功能是侦测ListBox控件某一被选中的索引,或者在ListBox控件的SelectionMode被设置成Multiple的时候侦测ListBox控件所有被选中的索引。

我们将这么实现LoadPostData方法。首先要保留原来方法的默认行为,即侦测被选中的索引,然后我们还要得到在上面所提到的两个隐藏字段中被保存的数据。

被保存的数据分别在两个隐藏字段中,“删除隐藏字段”保存被删除的项的value,“增加隐藏字段”保存增加项的text和value。

以下代码显示了LoadPostData是如何被实现的。

列表4
        bool IPostBackDataHandler.LoadPostData(string postDataKey, NameValueCollection postCollection)
        
{
            
// 处理被选中的value
            string[] postedItems = postCollection.GetValues(postDataKey);
            
bool returnValue = false;

            
// 如果没有被选中的项
            if (postedItems == null)
            
{
                
if (this.SelectedIndex != -1)
                
{
                    returnValue 
= true;
                }


                
// 处理客户端的变化去
                goto HandleClientChanges;
            }


            
// 如果SelectionMode是Single模式
            if (this.SelectionMode == ListSelectionMode.Single)
            
{
                
if (postedItems != null)
                
{
                    
// 处理postedItems的第一项,其是被选中的
                    int index = this.FindByValueInternal(postedItems[0]);
                    
if (this.SelectedIndex != index)
                    
{
                        
// 发生变化了
                        this.SelectedIndex = index;
                        returnValue 
= true;
                    }

                }

            }


            
// 否则SelectionMode是Multiple模式
            
// 新的被选中的Length
            int numberOfItemsSelected = postedItems.Length;

            
// 原来的被选中的索引集合
            ArrayList oldSelectedItems = this.SelectedIndicesInternal;

            
// 新集合,Length为新的被选中的Length
            ArrayList currentlySelectedItems = new ArrayList(numberOfItemsSelected);

            
// 把所有新的被选中的value塞进来
            for (int i = 0; i < numberOfItemsSelected; i++)
            
{
                currentlySelectedItems.Add(
this.FindByValueInternal(postedItems[i]));
            }


            
// 原来被选中的Length
            int numberOfSelectedItems = 0;
            
if (oldSelectedItems != null)
            
{
                numberOfSelectedItems 
= oldSelectedItems.Count;
            }


            
// 原来的和新的被选中的是否相同
            if (numberOfItemsSelected == numberOfSelectedItems)
            
{
                
for (int j = 0; j < numberOfItemsSelected; j++)
                
{
                    
int oldSelect = Convert.ToInt32(currentlySelectedItems[j]);
                    
int currentSelect = Convert.ToInt32(oldSelectedItems[j]);

                    
if (oldSelect != currentSelect)
                    
{
                        
// 标记该项被选中
                        this.Items[j].Selected = true;
                        returnValue 
= true;
                    }

                }

            }

            
else
            
{
                
// 原来的和新的被选中的发生了变化(原来的和新的被选中的Length不同就肯定是发生变化了)
                returnValue = true;
            }


            
// 有变化,重新设置Selected(设为新的)
            if (returnValue)
            
{
                
this.SelectInternal(currentlySelectedItems);
            }



        
// 这部分处理客户端的变化(项的增减)
        HandleClientChanges:

            
// 从项集合中删除项
            
// 处理客户端删除项操作
            string itemsRemoved = postCollection[this.HFItemsRemoved];
            
string[] itemsRemovedCol = itemsRemoved.Split(',');
            
if (itemsRemovedCol != null)
            
{
                
if ((itemsRemovedCol.Length > 0&& (itemsRemovedCol[0!= ""))
                
{
                    
for (int i = 0; i < itemsRemovedCol.Length; i++)
                    
{
                        ListItem itemToRemove 
= this.Items.FindByValue(itemsRemovedCol[i]);

                        
// 从集合中删除该项
                        Items.Remove(itemToRemove);
                    }

                    returnValue 
= true;
                }

            }


            
// 处理客户端增加项操作
            string itemsAdded = postCollection[this.HFItemsAdded];
            
string[] itemsCol = itemsAdded.Split(',');
            
if (itemsCol != null)
            
{
                
if ((itemsCol.Length > 0&& (itemsCol[0!= ""))
                
{
                    
// counter 用于确定返回值是什么
                    int counter = -1;
                    
for (int i = 0; i < itemsCol.Length; i++)
                    
{
                        
string buf = itemsCol[i];
                        
string[] itemsTokens = buf.Split('|');

                        
// 通过value检查是否已集合中已有该value
                        ListItem it = this.Items.FindByValue(itemsTokens[1]);
                        
if (it == null)
                        
{
                            
string text = itemsTokens[0];
                            
string id = itemsTokens[1];
                            ListItem item 
= new ListItem(text, id);
                            Items.Add(item);

                            
// 更新 counter
                            counter++;
                        }

                    }

                    returnValue 
= counter > -1 ? true : false;
                }

            }


            
return returnValue;
        }

这段代码首先实现了原来asp.net下的ListBox控件的LoadPostData方法

第二部分实现了通过用于保存删除项的隐藏控件存储的value从ListBox中删除项的功能

第三部分实现了通过用于保存增加项的隐藏控件存储的text|value从ListBox中增加项的功能

我们没有更多的说RaisePostDataChangedEvent方法,因为它的实现比较简单。

列表5
void IPostBackDataHandler.RaisePostDataChangedEvent()
{
  OnSelectedIndexChanged(EventArgs.Empty);
}

这个方法什么时候被调用呢?通过LoadPostData返回来的Boolean值来判断是否触发该方法。如果返回值为true,则证明被选中的值发生了变化,所以需要调用OnSelectedIndexChanged这个方法。

另外,如果LoadPostData返回的值为false,则证明选中的值没有发生变化,所以不用调用OnSelectedIndexChanged方法。

通过以上所说的,我们已经完成了一高级ListBox控件。接下来的部分,我们将看到一个例子来说明如何在webform中使用这个控件,如何调用前面所提及的那两个javascript函数。


如何使用新控件
在这一部分中,我们演示一个包含新的ListBox控件的webform,这个新的ListBox控件就是我们上面所创建的那个控件。

增加一个按钮来处理从ListBox中删除项的操作。

增加两个textbox控件,让用户输入为了插入ListBox里的text和value。另外,再增加一个按钮使新输入的项可以被插到ListBox中。

上面这些按钮都是客户端按钮,因此,ListBox的内部的各个项可以在客户端使用javascript来操作。

最后,一个服务端的按钮用于强制回发到服务端,同时ListBox各个项的变化都将被保存。

这个webfrom如下图所示。
图1

当你在ListBox中选择了一项,按下删除按钮时,所选择的项将从ListBox中删除,同时被删除的项的value将被添加到用于保存删除项的隐藏字段中。实现这个功能的代码如下。

列表6
    function RemoveItem()
    
{
        
// 得到ListBox
        var sourceListBox = document.getElementById('ListBox1');
        
        
// 检查ListBox是否为null
        if (sourceListBox != null)
        
{
            
// 获得被选项的value
            var selectedValue = sourceListBox.options[sourceListBox.options.selectedIndex].getAttribute("value");
            
            
// 从ListBox中删除该项
            sourceListBox.remove(sourceListBox.options.selectedIndex);
            
            
// 调用我们的ListBox输出到客户端的函数
            RemoveListItem(sourceListBox, selectedValue);            
        }

    }

你可以看到,这些代码已经有了自身的注释。当我们从ListBox中删除项的时候,我们已经可以检测到被选项了。最后,我们示例了一下如何调用新的ListBox中的那些客户端方法。

ListBox的ID和要被删除的项的value作为RemoveListItem需要的参数。

当你要向ListBox中增加项的时候,你只要简单的在两个textbox输入text和value,然后按“增加新项”按钮即可。这个按钮所触发的客户端代码如下:

列表7
    function AddItem()
    
{
        
// 得到ListBox
        var sourceListBox = document.getElementById('ListBox1');
        
var txt_text = document.getElementById('TextBox1');
        
var txt_value = document.getElementById('TextBox2');
        
        
// 检查ListBox是否为null
        if ((sourceListBox != null&& (txt_text != null&& (txt_value != null))
        
{
            
// 创建一个新项
            var newOption = new Option(); 
            newOption.text 
= txt_text.value;
            newOption.value 
= txt_value.value;
            
            
// 增加新创建的项到ListBox
            sourceListBox.options[sourceListBox.length] = newOption;
            
            
// 调用我们的ListBox输出到客户端的函数
            AddListItem(sourceListBox,newOption.text, newOption.value);
        }

     }

首先创建一个新项,然后把这个新项插入到ListBox中,之后再把新项增加到用于保存增加项的隐藏字段中。

在回发服务器后,你应该注意到ListBox中的各个项与发送服务端之前的各个项是相同的。这就证明这个控件已经达到了我们预期的功能。


下载
[原文源码下载]
[译者改后源码下载]


结论
在这篇文章里,我们一起知道了如何创建一个基于asp.net里的ListBox控件的新的自定义控件。

另外,我们一起了解了如何解决ListBox和所用继承自ListControl基类的控件的客户端操作无法在回传服务端后保存的问题。

希望你喜欢这篇文章

祝.net开发愉快。