Interacting controls within a Grid
I've been designing a website which has a grid on the page. The grid has multiple comboBoxes within it. These comboboxes interact. ie When one value is changed by the user another one has it's value change or is disabled/enabled etc.
I find in order to do this I have to use FindControl a lot. Like in the selectedindexchanged event of one combobox I'll need to find another comboBox.
This seems rather a messy way of doing things. It also seems like it leaves the system open to errors the compiler won't find say if a combobox has it's id changed later on down the line.
Can 开发者_开发技巧someone tell me is there a better way of going about this?
I have a web app that also makes extensive use of various FindControl permutations in order to accomplish what you describe. Although it is brittle (don't change control IDs without testing), it can be made slightly less cumbersome through some utility functions. Here are all the FindControl-type functions I use -- this may at least assist you.
/// <summary>
/// Recursively searches for a control within the heirarchy of a given control.
/// </summary>
/// <param name="root">The control to search within.</param>
/// <param name="id">The ID of the control to find.</param>
/// <returns>The Control object of the found control, or null if the control isn't found.</returns>
public static Control FindControlRecursive(Control root, string id)
{
if (root.ID == id) return root;
foreach (Control c in root.Controls)
{
Control t = FindControlRecursive(c, id);
if (t != null) return t;
}
return null;
}
/// <summary>
/// Recursively searches for a control within the heirarchy of a given control using the Client ID
/// of the control. Calling this too early in the lifecycle may not behave as expected.
/// </summary>
/// <param name="root">The control to search within.</param>
/// <param name="clientID">The Client ID of the control to find.</param>
/// <returns>The Control object of the found control, or null if the control isn't found.</returns>
public static Control FindControlRecursiveByClientID(Control root, string clientID)
{
if (0 == String.Compare(root.ClientID, clientID, true)) return root;
foreach (Control c in root.Controls)
{
Control t = FindControlRecursiveByClientID(c, clientID);
if (t != null) return t;
}
return null;
}
/// <summary>
/// Recursively searches for a group of controls within the heirarchy of a given control tree using the ID
/// of the control.
/// </summary>
/// <param name="root">The control tree to search within.</param>
/// <param name="id">The ID of the control to find.</param>
/// <returns>
/// A collection of the found controls. The collection will be empty if none are found.
/// </returns>
public static List<Control> FindControlsRecursive(Control root, string id)
{
List<Control> collection = new List<Control>();
FindControlRecursive(root, id, collection);
return collection;
}
private static void FindControlRecursive(Control root, string id, List<Control> collection)
{
foreach (Control c in root.Controls)
{
if (0 == String.Compare(c.ID, id, true)) collection.Add(c);
else FindControlRecursive(c, id, collection);
}
}
/// <summary>
/// Recursively searches for a control within the heirarchy of a given control using the type
/// of the control.
/// </summary>
/// <typeparam name="T">The type of the control to find.</typeparam>
/// <param name="root">The control to search within.</param>
/// <returns>
/// The Control object of the found control, or null if the control isn't found.
/// </returns>
public static T FindControlRecursiveByType<T>(Control root)
where T : Control
{
if (root is T) return (T)root;
foreach (Control c in root.Controls)
{
Control t = FindControlRecursiveByType<T>(c);
if (t != null) return (T)t;
}
return null;
}
/// <summary>
/// Recursively searches for a set of controls within the heirarchy of a given control using the type
/// of the control.
/// </summary>
/// <typeparam name="T">The type of the control to find.</typeparam>
/// <param name="root">The control to search within.</param>
/// <returns>
/// A generic List object containing the controls found, or an empty List of none were found.
/// </returns>
public static List<T> FindControlsRecursiveByType<T>(Control root)
where T : Control
{
List<T> collection = new List<T>();
FindControlRecursiveByType<T>(root, collection);
return collection;
}
private static void FindControlRecursiveByType<T>(Control root, List<T> collection)
where T : Control
{
foreach (Control c in root.Controls)
{
if (c is T) collection.Add((T)c);
else FindControlRecursiveByType<T>(c, collection);
}
}
It is for this very reason I switched from ASP.NET to developing in Silverlight and leveraging its MVVM pattern. Even with ASP.NET's GridView item templates, each item cannot be directly bound to, be made aware of, or reference another item in the same template. Your code behind must know, to some degree (which usually is the fullest), the composition hierarchy of your view controls.
Here's what you could do to get closer to the "better binding world" though. You would still bind your combo boxes to the same list data sources, but when each row of items/controls is created, you would associate each item with an object (i.e. tag item). Then in your controls' event handling, you would retrieve other controls associated with the tag item associated with the control that raised the event and do what you will with them.
I know, not the best idea, but just off the top of my head. Maybe when I've had time to think more on this I can update this post.
How about using Events to do the notifying?
These are very messy.
Here is a very simple and elegant solution. Say your grid needs to display data in a table with 3 columns. The data is coming from an object with the following structure:
[Serializable]
public class Foo
{
public string Bar1 { get; set; }
public string Bar2 { get; set; }
public string Bar3 { get; set; }
}
Then your user control will have the following markup:
Markup (GridDisplayRow.ascx):
<%@ Control Language="C#" AutoEventWireup="true" CodeFile="GridDisplayRow.ascx.cs" Inherits="GridDisplayRow" %>
<div>
<div class="cell">
<asp:TextBox id="TxtProperty1" runat="server"/>
</div>
<div class="cell">
<asp:DropDownList ID="DDLProperty2" runat="server" OnSelectedIndexChanged="DDLProperty2_OnSelectedIndexChanged" AutoPostBack="true">
</asp:DropDownList>
</div>
<div class="cell">
<input type="radio" id="RadProperty3" runat="server">
</div>
</div>
Notice all the div tags. The children div's are floated... so that they display side by side.
Codebehind (GridDisplayRow.ascx.cs):
class GridDisplayRow:UserControl
{
public event EventHandler<System.EventArgs> SomethingHappened;
protected void Page_Load(object sender, EventArgs e)
{
//Initialize the drop down with something;
}
//this is where we handle internal events generated by children controls.
//Eg: the drop down's index changed.
protected void DDLProperty2_OnSelectedIndexChanged(object sender, EventArgs e)
{
//we update some other child control in this...
this.TxtProperty1.Text = this.DDLProperty2.Value;
//and maybe we want to signal the page that some event happened
if(SomethingHappened != null)
{
//Notify the page that SomethingHappened event occured
SomethingHappened(this, EventArgs.Empty);
}
}
//This is where the binding happens
public object BindingObject
{
set
{
Foo temp = (Foo)value;
this.TxtProperty1.Text = temp.Bar1;
this.DDLProperty2.Value = temp.Bar2;
this.RadProperty3.Value = temp.Bar3;
this.ViewState["Foo"] = temp;
}
}
}
So in the code above we are handling the binding to Foo, in other words we are displaying Foo's properties in each cell (div) of the control. The cast is needed because the property above is of type object, and in the ItemTemplate of the Grid/Repeater/What have you, you will bind an instance of the GridDisplayRow to the Container.DataItem object as shown bellow. Just keep in mind that if your data source is a DataSet for example, you have to cast to DataRow, or whatever appropriate data type is needed:
Page Markup:
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Test.aspx.cs" Inherits="Test" %>
<%@ Register src="GridDisplayRow.ascx" tagname="GridRow" tagprefix="ctrl" %>
<asp:GridView ID="GridView1" runat="server">
<Columns>
<asp:TemplateField>
<HeaderTemplate>
<div>
<div class="header cell">Header 1</div>
<div class="header cell">Header 2</div>
<div class="header cell">Header 3</div>
</div>
</HeaderTemplate>
<ItemTemplate>
<ctrl:GridRow ID="Row" runat="server" BindingObject='<%# Container.DataItem %>' OnSomethingHappened="Row_SomethingHappened" />
</ItemTemplate>
</asp:TemplateField>
</Columns>
</asp:GridView>
I don't like working with grids because they are messy and cumbersome. In this instance we tricked the grid to create a Table with a single cell per row, and took care of the layout within that cell via style sheets.
Please note that this works better with a Repeater as it is super efficient and has less overhead!
Now binding the grid: Any source that implements IEnumerable can be used to bind to the grid. As such a collection such as List will suffice.
When the GridRow control fires its events, a reference to the control itself is sent via the object sender, thus if you cast sender to its right type you can grab the control's inner properties.... possibilities are endless.
The advantage of such method is abstraction. Each control can handle its own events, or chose to notify the page of events that occured inside of it.... bla bla bla. You get the idea.
Try using a ListView
is more easy to manage that kind of requirements. If you put the ListView
inside an Update Panel
you can have a better user experience
精彩评论