WebControls 中的数据绑定






4.90/5 (40投票s)
一篇关于如何在 WebControl 中实际支持数据绑定的文章,以便您可以在属性窗口中操作它们。
引言
在过去的几个月里,我一直在创建各种 WebControls,其中一些需要访问数据源,例如 DataSet,以便正常工作。为 WebControl 添加数据源相对容易,但棘手的部分是当您需要与属性窗口中的该属性进行交互时,例如,以与 DropDownList 控件相同的方式。现在,如果您幸运并且在 MSDN 中正确查询,您会找到以下文章 Implementing a Web Forms Data-Bound Control Designer。然而,您会像我一样注意到示例并没有像宣传的那样工作。在尝试调试时,我在 microsoft.public.dotnet.framework.aspnet.webcontrols 新闻组上发布了一个消息,并收到了 Mike Moore "DataSource property and DataBinding thread" 的以下回复,尽管它并不是我想要的 100%,但它让我走上了正确的轨道。然而,Mike Moore 提供的源代码中有一个有趣的评论(请参阅 `IDataSourceProvider` 接口的 supplied source),这促使我进一步研究如何为 WebControl 添加数据绑定功能,以便您可以在属性窗口中操作它们。
我打算做的是解释每个步骤,并描述您实际需要做什么才能支持 WebControl 中的数据绑定,以与 MSDN 示例中说您需要做的进行对比。在此演示中,我创建了一个非常简单的控件,它除了公开可用于绑定到数据源的属性之外,什么也不做,该数据源中的一个表,最后是驻在该表中的一些字段,其方式与列表框相同。
一个简单的数据绑定控件及其设计器
可下载的源代码包含 `SimpleDataBoundControl` 的代码,其类视图如下所示。
现在,为了在属性窗口中支持数据绑定,我们需要为控件添加一个设计器。正是这个设计器在您设计控件时完成了所有繁重的工作。`SimpleDataBoundControlDesigner` 的最终实现的源代码也包含在下载中。为了将 WebControl 与其设计器链接,我们使用 `Designer` 类属性,因为我更喜欢将控件及其相关的设计器保留在同一个库中,所以我发现使用 `typeof` 版本更容易。
[DefaultProperty("DataSource"),
ToolboxData("<{0}:SimpleDataBoundControl runat=server></{0}:SimpleDataBoundControl>"),
Designer(
typeof(ManyMonkeys.Web.ControlLibrary.Design.SimpleDataBoundControlDesigner))]
public class SimpleDataBoundControl : System.Web.UI.WebControls.WebControl ,
INamingContainer
{
...
}
DataSource 属性
`DataSource` 是用于绑定到如 `DataSet` 之类的数据源的属性的通用成员名称。在绑定其他属性之前,我们需要确保此属性已正确设置。我们使用设计器来允许我们将 `DataSource`(这是一个 `object` 类型属性)表示为 `string` 类型。在设计器类中,我们添加了一个 `DataSource` 属性,它是一个 `string` 类型,看起来像下面的摘录。
public class SimpleDataBoundControlDesigner : ...
{
...
public string DataSource
{
get
{
DataBinding binding = DataBindings["DataSource"];
if (binding != null)
return binding.Expression;
return string.Empty;
}
set
{
if ((value == null) || (value.Length == 0))
base.DataBindings.Remove("DataSource");
else
{
DataBinding binding = DataBindings["DataSource"];
if (binding == null)
binding = new DataBinding("DataSource", typeof(IEnumerable),
value);
else
binding.Expression = value;
DataBindings.Add(binding);
}
OnBindingsCollectionChanged("DataSource");
}
}
}
我们还需要为上述属性添加一个名为 `DataSourceConverter` 的类型转换器,以便它能够正确枚举 WebForm 上可用的数据源,并在属性窗口中将其呈现为组合框。要添加此转换器,我们需要重写 `PreFilterProperties` 方法,并在设计时动态地将 `TypeConverter` 属性添加到 `DataSource` 属性。
public class SimpleDataBoundControlDesigner : ...
{
...
protected override void PreFilterProperties(IDictionary properties)
{
base.PreFilterProperties(properties);
PropertyDescriptor prop = (PropertyDescriptor)properties["DataSource"];
if(prop!=null)
{
AttributeCollection runtimeAttributes = prop.Attributes;
// make a copy of the original attributes but make room for one extra
// attribute ie the TypeConverter attribute
Attribute[] attrs = new Attribute[runtimeAttributes.Count + 1];
runtimeAttributes.CopyTo(attrs, 0);
attrs[runtimeAttributes.Count] = new
TypeConverterAttribute(typeof(DataSourceConverter));
prop = TypeDescriptor.CreateProperty(this.GetType(), "DataSource",
typeof(string),attrs);
properties["DataSource"] = prop;
}
}
}
在 `SimpleDataBoundControl` 中实现 `DataSource` 属性的最后一步是包含 `DesignerSerializationVisibility` 属性,以便数据源以如下样式保存在 HTML 中:`DataSource="<%# dataSet11%>"` 而不是 `DataSource="dataset11"`。
public class SimpleDataBoundControl : ...
{
...
private object _dataSource=null;
[
Bindable(true),
Category("Data"),
DefaultValue(null),
Description("The datasource that is used to populate the list with items."),
// needs to be hidden otherwise we don't save the property for some reason
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)
]
public object DataSource
{
get { return _dataSource; }
set
{
if ((value == null) || (value is IListSource) || (value is IEnumerable))
_dataSource = value;
else
throw new Exception("Invalid datasource.");
}
}
}
现在我们应该拥有一个控件,我们可以从中从可用数据源列表中选择一个数据源。
IDataSourceProvider 接口
正如您所看到的,没有必要实现 MSDN 示例中描述的 `IDataSourceProvider` 接口,也没有实现新闻组提供的示例中暗示的接口。事实上,如果您阅读 MSDN 关于 `IDataSourceProvider` 的文档,您会看到只有在使用 `DataMemberConverter` 和 `DataFieldConverter` 类型转换器时才需要它。示例不使用 `IDataSourceProvider` 的事实将解释为什么当我们尝试实现使用 `DataMemberConverter` 和 `DataFieldConverter` 的属性时,给出的代码不起作用,因为它可能从未经过测试和调试。
实现 `IDataSourceProvider` 接口的设计器需要提供两个方法
- `GetSelectedDataSource`,它似乎仅由 `DataMemberConverter` 使用 和
- `GetResolvedSelectedDataSource`,它似乎仅由 `DataFieldConverter` 使用
由于需要先实现使用 `DataMemberConverter` 的属性,然后再实现使用 `DataFieldConverter` 的属性,因此我们将在此时间处理 `IDataSourceProvider` 的 respective 方法实现。
DataMember 属性
`DataMember` 用于从提供的如 `DataSet` 之类的数据源中选择一个特定表,如果为空,则控件应使用第一个可用的表或 `DataView`。在设计器类(在本例中为 `SimpleDataBoundControlDesigner`)中完成了实现 `DataMember` 属性的所有工作,以便在属性窗口中将其表示为带有可用表列表的组合框。
首先,我们需要为 `DataMember` 创建一个属性,我们将用它来附加类型转换器属性。
public class SimpleDataBoundControlDesigner : ...
{
...
public string DataMember
{
get
{
return ((SimpleDataBoundControl)this.Component).DataMember;
}
set
{
((SimpleDataBoundControl)this.Component).DataMember = value;
}
}
}
然后,在 `PreFilterProperties` 中,我们以与 `DataSource` 属性相同的方式附加一个类型转换器,在本例中是 `DataMemberConverter`。
public class SimpleDataBoundControlDesigner : ...
{
...
protected override void PreFilterProperties(IDictionary properties)
{
...
prop = (PropertyDescriptor)properties["DataMember"];
if(prop!=null)
{
AttributeCollection runtimeAttributes = prop.Attributes;
// make a copy of the original attributes but make room for one extra
// attribute ie the TypeConverter attribute
Attribute[] attrs = new Attribute[runtimeAttributes.Count + 1];
runtimeAttributes.CopyTo(attrs, 0);
attrs[runtimeAttributes.Count] = new TypeConverterAttribute(
typeof(DataMemberConverter));
prop = TypeDescriptor.CreateProperty(this.GetType(), "DataMember",
typeof(string),attrs);
properties["DataMember"] = prop;
}
}
}
现在,为了使 `DataMemberConverter` 正确工作,我们需要实现 `IDataSourceProvider` 接口,特别是 `GetSelectedDataSource()` 方法。不幸的是,MSDN 示例中提供的实现在使用 `DataSet` 时不起作用,因为 `DataSet` 不支持 `IEnumerable`,但它支持 `IListSource`。所以我们可以将此接口添加到 MSDN 提供的代码中的检查中(参见下面的注释)。
public class SimpleDataBoundControlDesigner : ...
{
...
object IDataSourceProvider.GetSelectedDataSource()
{
object selectedDataSource = null;
string dataSource = null;
DataBinding binding = DataBindings["DataSource"];
if (binding != null)
{
dataSource = binding.Expression;
}
if (dataSource != null)
{
ISite componentSite = Component.Site;
if (componentSite != null)
{
IContainer container = (IContainer)componentSite.GetService(
typeof(IContainer));
if (container != null)
{
IComponent comp = container.Components[dataSource];
// Added the IListSource test as DataSet doesn't
// support IEnumerable
if ((comp is IEnumerable) || (comp is IListSource))
{
selectedDataSource = comp;
}
}
}
}
return selectedDataSource;
}
IEnumerable IDataSourceProvider.GetResolvedSelectedDataSource()
{
return null;
}
}
我们仅为 `GetResolvedSelectedDataSource` 方法实现了一个简单的实现,因为它似乎不需要 `DataMemberConverter` 来工作。
现在我们应该拥有一个控件,我们可以从中通过列表框从所选数据源的可用表中选择一个表。
DataTextField 和 DataValueField 属性
`DataTextField` 和 `DataValueField` 属性用于从默认表或预选数据源中的选定表中选择一个特定字段。同样,允许我们从可用字段列表中选择所需的所有工作也在设计器类中完成。我们再次添加一个属性,用于将所需的类型转换器(在本例中为 `DataFieldConverter`)附加到该属性,并且我们还在 `PreFilterProperties` 方法中将类型转换器添加到属性中。
public class SimpleDataBoundControlDesigner : ... { ... public string DataTextField { get { return ((SimpleDataBoundControl)this.Component).DataTextField; } set { ((SimpleDataBoundControl)this.Component).DataTextField = value; } } public string DataValueField { get { return ((SimpleDataBoundControl)this.Component).DataValueField; } set { ((SimpleDataBoundControl)this.Component).DataValueField = value; } } protected override void PreFilterProperties(IDictionary properties) { ... prop = (PropertyDescriptor)properties["DataTextField"]; if(prop!=null) { AttributeCollection runtimeAttributes = prop.Attributes; Attribute[] attrs = new Attribute[runtimeAttributes.Count + 1]; // make a copy of the original attributes but make room for one extra // attribute ie the TypeConverter attribute runtimeAttributes.CopyTo(attrs, 0); attrs[runtimeAttributes.Count] = new TypeConverterAttribute( typeof(DataFieldConverter)); prop = TypeDescriptor.CreateProperty(this.GetType(), "DataTextField", typeof(string),attrs); properties["DataTextField"] = prop; } prop = (PropertyDescriptor)properties["DataValueField"]; if(prop!=null) { AttributeCollection runtimeAttributes = prop.Attributes; Attribute[] attrs = new Attribute[runtimeAttributes.Count + 1]; // make a copy of the original attributes but make room for one extra // attribute ie the TypeConverter attribute runtimeAttributes.CopyTo(attrs, 0); attrs[runtimeAttributes.Count] = new TypeConverterAttribute( typeof(DataFieldConverter)); prop = TypeDescriptor.CreateProperty(this.GetType(), "DataValueField", typeof(string),attrs); properties["DataValueField"] = prop; } } }
现在,唯一需要的是为 `IDataSourceProvider` 实现 `GetResolvedSelectedDataSource` 方法。MSDN 示例提供了以下实现。
IEnumerable IDataSourceProvider.GetResolvedSelectedDataSource() { return (IEnumerable)((IDataSourceProvider)this).GetSelectedDataSource(); }
然而,如前所述,`DataSet` 不支持 `IEnumerable`,因此上述代码会导致异常并不起作用。为了使其对 `DataSet` 工作,我们需要深入到 `DataSet` 中存在的 `DataView`,我们还需要根据 `DataMember` 属性中已预选的表来选择 `DataView`。以下实现已在 `DataSet` 上进行了测试并能正常工作。
public class SimpleDataBoundControlDesigner : ... { ... IEnumerable IDataSourceProvider.GetResolvedSelectedDataSource() { object selectedDataSource = ((IDataSourceProvider)this).GetSelectedDataSource(); DataView dataView = null; if (selectedDataSource is DataSet) { // find the correct table or if non set the look up the first table DataSet dataSet = (DataSet)selectedDataSource; DataTable dataTable = null; if ((DataMember != null) && (DataMember.Length>0)) dataTable = dataSet.Tables[DataMember]; else dataTable=dataSet.Tables[0]; // we found a table so lets just get its default view if (dataTable!=null) { dataView = dataTable.DefaultView; } } else if (selectedDataSource is DataTable) { // just get the default view since we have just been given a table dataView = ((DataTable)selectedDataSource).DefaultView; } else if (selectedDataSource is IEnumerable) { // might as well just see if it will cast as this is // the MS sample's default return selectedDataSource as IEnumerable; } return dataView as IEnumerable; } }
现在我们应该拥有一个控件,我们可以从中选择一个已选表中的一个字段。
DesignTimeData 类
`.NET` 框架中存在 `DesignTimeData` 类,可用于实现 `IDataSourceProvider` 接口的方法。下面的代码使用 `DesignTimeData` 类展示了一个更简单的 `IDataSourceProvider` 接口实现所需方法的实现。
public class SimpleDataBoundControlDesigner : ... { ... object IDataSourceProvider.GetSelectedDataSource() { DataBinding binding; binding = this.DataBindings["DataSource"]; if (binding != null) return DesignTimeData.GetSelectedDataSource(this.Component, binding.Expression); return null; } IEnumerable IDataSourceProvider.GetResolvedSelectedDataSource() { DataBinding binding; binding = this.DataBindings["DataSource"]; if (binding != null) return DesignTimeData.GetSelectedDataSource(this.Component, binding.Expression, this.DataMember); return null; } }
注释
请花时间为本文投票,并/或在下面的论坛中发表评论。所有改进建议都将得到考虑。
历史
- 92/10/02 - 初始版本