Download source files - 13.5 Kb
Download demo project - 26.8 Kb
介绍
这篇文章展示了如何创建一个自定义的数据源控件和怎样给它增加完整的设计时支持
背景
这篇文章假定你已经很熟悉数据源控件(DataSourceControl)和你知道设计时基础工作原理。如果你对这些知识不了解,关注一下下面的文章。
关于数据源控件
关于设计时的基础知识
创建自定义的数据源控件
我们将要编写的数据源是只可以取得数据,但是不能编辑数据。它只支持SELECT操作。它很像ObjectDataSource,但是只是返回数据。它包含一个TypeName属性,用来保存一个类名,包含一个SelectMethod属性,用来保存类中可以调用的方法。为了避免写过多的代码,我们只找出类中的静态方法。我们还有一个参数集合来传递SelectMethod(SelectParameters).在创建数据源控件的同时我将解释将执行的主要任务,但我不详细解释某方法或者某属性具体做什么。
要做的第一件事情是当实现一个DataSourceControl时,我们要选择多少DataSourceViews和如何编写与IDataSource有关的方法。在这个例子中,我们只有一个View:
{
protected static readonly string[] _views = { "DefaultView" };
protected CustomDataSourceView _view;
protected override DataSourceView GetView(string viewName)
{
if ((viewName == null) || ((viewName.Length != 0) &&
(String.Compare(viewName, "DefaultView",
StringComparison.OrdinalIgnoreCase) != 0)))
{
throw new ArgumentException("An invalid view was requested",
"viewName");
}
return View;
}
protected override ICollection GetViewNames()
{
return _views;
}
protected CustomDataSourceView View
{
get
{
if (_view == null) {
_view = new CustomDataSourceView(this, _views[0]);
if (base.IsTrackingViewState) {
((IStateManager)_view).TrackViewState();
}
}
return _view;
}
}
}
CustomDataSource类作为完成我们全部工作的类,最好我们把需要的属性存储在这里。但是我们需要暴露这些属性在CustomDataSource好让用户去编辑他们在属性编辑器中。因此,我们必须在CustomDataSource类中也添加。
public string TypeName
{
get { return View.TypeName; }
set { View.TypeName = value; }
}
[Category("Data"), DefaultValue("")]
public string SelectMethod
{
get { return View.SelectMethod; }
set { View.SelectMethod = value; }
}
[PersistenceMode(PersistenceMode.InnerProperty), Category("Data"),
DefaultValue((string)null), MergableProperty(false),
Editor(typeof(ParameterCollectionEditor),
typeof(UITypeEditor))]
public ParameterCollection SelectParameters
{
get { return View.SelectParameters; }
}
添加这些到CustomDataSouceView类中
{
protected bool _tracking;
protected CustomDataSource _owner;
protected string _typeName;
protected string _selectMethod;
protected ParameterCollection _selectParameters;
public string TypeName
{
get
{
if (_typeName == null) {
return String.Empty;
}
return _typeName;
}
set
{
if (TypeName != value) {
_typeName = value;
OnDataSourceViewChanged(EventArgs.Empty);
}
}
}
public string SelectMethod
{
get
{
if (_selectMethod == null) {
return String.Empty;
}
return _selectMethod;
}
set
{
if (SelectMethod != value) {
_selectMethod = value;
OnDataSourceViewChanged(EventArgs.Empty);
}
}
}
public ParameterCollection SelectParameters
{
get
{
if (_selectParameters == null)
{
_selectParameters = new ParameterCollection();
_selectParameters.ParametersChanged +=
new EventHandler(ParametersChangedEventHandler);
if (_tracking)
{
((IStateManager)_selectParameters).TrackViewState();
}
}
return _selectParameters;
}
}
protected void ParametersChangedEventHandler(object o, EventArgs e)
{
OnDataSourceViewChanged(EventArgs.Empty);
}
public CustomDataSourceView(CustomDataSource owner, string name)
: base(owner, name)
{
_owner = owner;
}
}
注意,当属性改变的时候,OnDataSourceViewChanged 方法被调用去强制重新绑定。同时注意到CustomDataSourceView类实现了IStateManager接口,可以支持定制View 状态管理。既然这样,我们用它去存储SelectParameters。状态管理在CustomDataSource类中这样写:
{
Pair previousState = (Pair) savedState;
if (savedState == null)
{
base.LoadViewState(null);
}
else
{
base.LoadViewState(previousState.First);
if (previousState.Second != null)
{
((IStateManager) View).LoadViewState(previousState.Second);
}
}
}
protected override object SaveViewState()
{
Pair currentState = new Pair();
currentState.First = base.SaveViewState();
if (_view != null)
{
currentState.Second = ((IStateManager) View).SaveViewState();
}
if ((currentState.First == null) && (currentState.Second == null))
{
return null;
}
return currentState;
}
protected override void TrackViewState()
{
base.TrackViewState();
if (_view != null)
{
((IStateManager) View).TrackViewState();
}
}
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
// handle the LoadComplete event to update select parameters
if (Page != null)
{
Page.LoadComplete += new EventHandler(UpdateParameterValues);
}
}
我们用Pair去存储状态,第一个对象用来存取父视图状态,第二个对象用来存储当前视图的视图状态。关于CustomDataSouceView,状态管理这样写:
{
get { return _tracking; }
}
void IStateManager.LoadViewState(object savedState)
{
LoadViewState(savedState);
}
object IStateManager.SaveViewState()
{
return SaveViewState();
}
void IStateManager.TrackViewState()
{
TrackViewState();
}
protected virtual void LoadViewState(object savedState)
{
if (savedState != null)
{
if (savedState != null)
{
((IStateManager)SelectParameters).LoadViewState(savedState);
}
}
}
protected virtual object SaveViewState()
{
if (_selectParameters != null)
{
return ((IStateManager)_selectParameters).SaveViewState();
}
else
{
return null;
}
}
protected virtual void TrackViewState()
{
_tracking = true;
if (_selectParameters != null)
{
((IStateManager)_selectParameters).TrackViewState();
}
}
我们必须求出SelectParameters的值在每一次请求,因为如果参数改变,我们被需重新绑定:
{
base.OnInit(e);
// handle the LoadComplete event to update select parameters
if (Page != null)
{
Page.LoadComplete += new EventHandler(UpdateParameterValues);
}
}
protected virtual void UpdateParameterValues(object sender, EventArgs e)
{
SelectParameters.UpdateValues(Context, this);
}
最后我们要做的就是完成在CustomDataSourceView确切的选取部分:
DataSourceSelectArguments arguments)
{
// if there isn't a select method, error
if (SelectMethod.Length == 0)
{
throw new InvalidOperationException(
_owner.ID + ": There isn't a SelectMethod defined");
}
// check if we support the capabilities the data bound control expects
arguments.RaiseUnsupportedCapabilitiesError(this);
// gets the select parameters and their values
IOrderedDictionary selParams =
SelectParameters.GetValues(System.Web.HttpContext.Current, _owner);
// gets the data mapper
Type type = BuildManager.GetType(_typeName, false, true);
if (type == null)
{
throw new NotSupportedException(_owner.ID + ": TypeName not found!");
}
// gets the method to call
MethodInfo method = type.GetMethod(SelectMethod,
BindingFlags.Public | BindingFlags.Static);
if (method == null)
{
throw new InvalidOperationException(
_owner.ID + ": SelectMethod not found!");
}
// creates a dictionary with the parameters to call the method
ParameterInfo[] parameters = method.GetParameters();
IOrderedDictionary paramsAndValues =
new OrderedDictionary(parameters.Length);
// check that all parameters that the method needs are
// in the SelectParameters
foreach (ParameterInfo currentParam in parameters)
{
string paramName = currentParam.Name;
if (!selParams.Contains(paramName))
{
throw new InvalidOperationException(_owner.ID +
": The SelectMethod doesn't have a parameter for " +
paramName);
}
}
// save the parameters and its values into a dictionary
foreach (ParameterInfo currentParam in parameters)
{
string paramName = currentParam.Name;
object paramValue = selParams[paramName];
if (paramValue != null)
{
// check if we have to convert the value
// if we have a string value that needs conversion
if (!currentParam.ParameterType.IsInstanceOfType(paramValue) &&
(paramValue is string))
{
// try to get a type converter
TypeConverter converter =
TypeDescriptor.GetConverter(currentParam.ParameterType);
if (converter != null)
{
try
{
// try to convert the string using the type converter
paramValue = converter.ConvertFromString(null,
System.Globalization.CultureInfo.CurrentCulture,
(string)paramValue);
}
catch (Exception)
{
throw new InvalidOperationException(
_owner.ID + ": Can't convert " +
paramName + " from string to " +
currentParam.ParameterType.Name);
}
}
}
}
paramsAndValues.Add(paramName, paramValue);
}
object[] paramValues = null;
// if the method has parameters, create an array to
// store parameters values
if (paramsAndValues.Count > 0)
{
paramValues = new object[paramsAndValues.Count];
for (int i = 0; i < paramsAndValues.Count; i++)
{
paramValues[i] = paramsAndValues[i];
}
}
object returnValue = null;
try
{
// call the method
returnValue = method.Invoke(null, paramValues);
}
catch (Exception e)
{
throw new InvalidOperationException(
_owner.ID + ": Error calling the SelectMethod", e);
}
return (IEnumerable)returnValue;
}</PRE>
这些代码远不是产品化的代码,例如,它们可能有重载好几种同样的SelectMethod,使用不同的参数。参数转换不处理Reference和generic类型。也不支持DataSet和DataTable类型,因为它们不是实现的IEnumerable。你也许要在DataView下面去扩展它们。不管怎样,添加这些“扩展特性”会使得代码复杂,更加难理解。
下载我们来为我们的CustomDataSouce控件来创建设计器。主要的工作是完成DataSouceDesigner,包括:
配置数据源
暴露架构(Schema)信息
因此,我们需要暴露至少一个DesignerDataSouceView。一个DataSouce控件要暴露一个或者多个DataSourceView,一个DataSouceDesigner要暴露一个或多DesignerDataSouceViews:
public override DesignerDataSourceView GetView(string viewName)
{
if ((viewName == null) || ((viewName.Length != 0) &&
(String.Compare(viewName, "DefaultView",
StringComparison.OrdinalIgnoreCase) != 0)))
{
throw new ArgumentException("An invalid view was requested",
"viewName");
}
return View;
}
public override string[] GetViewNames()
{
return _views;
}
你看到了,代码和我们写过的在Data Souce中暴露DataSouce view的代码很相似。因为我们的数据源只返回数据,默认DesignerDataSourceView实现就可以满足全部的CanXXX属性。为了快速的配置我们的自定义的数据源,我们提供了一个图形界面(GUI)可以让我们从下拉框中选取TypeName和SelectMethod:
为了可以显示配置数据源对话窗口,我们必须重载CanConfigure属性和实现Congigure方法:
{
get { return true; }
}
public override void Configure()
{
_inWizard = true;
// generate a transaction to undo changes
InvokeTransactedChange(Component,
new TransactedChangeCallback(ConfigureDataSourceCallback),
null, "ConfigureDataSource");
_inWizard = false;
}
protected virtual bool ConfigureDataSourceCallback(object context)
{
try
{
SuppressDataSourceEvents();
IServiceProvider provider = Component.Site;
if (provider == null)
{
return false;
}
// get the service needed to show a form
IUIService UIService =
(IUIService) provider.GetService(typeof(IUIService));
if (UIService == null)
{
return false;
}
// shows the form
ConfigureDataSource configureForm =
new ConfigureDataSource(provider, this);
if (UIService.ShowDialog(configureForm) == DialogResult.OK)
{
OnDataSourceChanged(EventArgs.Empty);
return true;
}
}
finally
{
ResumeDataSourceEvents();
}
return false;
}</PRE>
当GUI界面同时将要更改几个属性时,我们必须要建立一个事务改变来提供撤销功能。我们用一个type discovery服务代替反射机制来获得全部的类型填充窗口界面上的第一个下来框。为什么呢?因为用反射机制,我们只能得到全部已经编译的程序集类型,现在是我们要可以添加更多的类型即使还没有在工程中被编译。我们用type Discovery服务可以得到这些没有编译的并且显示他们,因此,用type discovery服务来代替反射要好的多。
在代码中,我没有移除那些可能对于我们的TypeName属性不能使用的,例如generic类型,接口--这样做是为了使代码尽可能简单:
{
// try to get a reference to the type discovery service
ITypeDiscoveryService discovery = null;
if (_component.Site != null)
{
discovery =
(ITypeDiscoveryService)_component.Site.GetService(
typeof(ITypeDiscoveryService));
}
// if the type discovery service is available
if (discovery != null)
{
// saves the cursor and sets the wait cursor
Cursor previousCursor = Cursor.Current;
Cursor.Current = Cursors.WaitCursor;
try
{
// gets all types using the type discovery service
ICollection types = discovery.GetTypes(typeof(object), true);
ddlTypes.BeginUpdate();
ddlTypes.Items.Clear();
// adds the types to the list
foreach (Type type in types)
{
TypeItem typeItem = new TypeItem(type);
ddlTypes.Items.Add(typeItem);
}
}
finally
{
Cursor.Current = previousCursor;
ddlTypes.EndUpdate();
}
}
}
TypeItem类是用来存储下拉框中的数据类型。当一个类型被选中从第一个下拉框列表,另外一个下拉框列表将得到算中类型的全部方法。
{
// saves the cursor and sets the wait cursor
Cursor previousCursor = Cursor.Current;
Cursor.Current = Cursors.WaitCursor;
try
{
// gets all public methods (instance + static)
MethodInfo[] methods =
CustomDataSourceDesigner.GetType(_component.Site, TypeName).
GetMethods(BindingFlags.Public | BindingFlags.Static |
BindingFlags.Instance | BindingFlags.FlattenHierarchy);
ddlMethods.BeginUpdate();
ddlMethods.Items.Clear();
// adds the methods to the dropdownlist
foreach (MethodInfo method in methods)
{
MethodItem methodItem = new MethodItem(method);
ddlMethods.Items.Add(methodItem);
}
}
finally
{
Cursor.Current = previousCursor;
ddlMethods.EndUpdate();
}
}
为了快速取得和设置TypeName和SelectMethod从窗体上,我们定义了下面的一些属性:
{
get
{
// gets the selected type
TypeItem selectedType = ddlTypes.SelectedItem as TypeItem;
// return the selected type
if (selectedType != null)
{
return selectedType.Name;
}
else
{
return String.Empty;
}
}
set
{
// iterate through all the types searching for the requested type
foreach (TypeItem item in ddlTypes.Items)
{
// if we have found it, select it
if (String.Compare(item.Name, value, true) == 0)
{
ddlTypes.SelectedItem = item;
break;
}
}
}
}
internal string SelectMethod
{
get
{
// gets the select method
string methodName = String.Empty;
if (MethodInfo != null)
{
methodName = MethodInfo.Name;
}
return methodName;
}
set
{
// iterate through all the types searching for the requested type
foreach (MethodItem item in ddlMethods.Items)
{
// if we have found it, select it
if (String.Compare(item.MethodInfo.Name, value, true) == 0)
{
ddlMethods.SelectedItem = item;
break;
}
}
}
}
internal MethodInfo MethodInfo
{
get
{
MethodItem item = ddlMethods.SelectedItem as MethodItem;
if (item == null)
{
return null;
}
return item.MethodInfo;
}
}
注意这里为了简化代码,当SelectMethod属性被设置时,Select Method下拉列表中显示的是第一个方法,但是为了简化代码,认为没有参数被选中,如果作为产品你必须要去检查是参数匹配。
在FillMethod方法中,type是用GetType方法中获得的,使用的resolution服务。使用这个服务的道理和前面使用type discovery服务的道理是一样的。
string typeName)
{
// try to get a reference to the resolution service
ITypeResolutionService resolution =
(ITypeResolutionService)serviceProvider.
GetService(typeof(ITypeResolutionService));
if (resolution == null)
{
return null;
}
// try to get the type
return resolution.GetType(typeName, false, true);
}
当用户在配置数据源窗口点击接受按钮,下面的代码将执行:
{
// if the type has changed, save it
if (String.Compare(TypeName, _component.TypeName, false) != 0)
{
TypeDescriptor.GetProperties(
_component)["TypeName"].SetValue(_component, TypeName);
}
// if the select method has changed, save it
if (String.Compare(SelectMethod, _component.SelectMethod, false) != 0)
{
TypeDescriptor.GetProperties(
_component)["SelectMethod"].SetValue(_component, SelectMethod);
}
// if there is method selected, refresh the schema
if (MethodInfo != null)
{
_designer.RefreshSchemaInternal(MethodInfo.ReflectedType,
MethodInfo.Name,
MethodInfo.ReturnType, true);
}
}
我们保存Type和SelectMethod并且刷新schema。为了提供schema信息,我们必须在CanRefreshSchema方法中返回true,还要实现RefeshSchema方法。当我们提供Schema信息。控件可以提供字段读取者,例如GridView上的Columns;基于schema信息产生一个模板,例如一个DataList绑定到我们的数据源控件。然而,在这里我们不能在CanRefreshSchema中返回ture,因为我们只有当用户在配置数据源的时候才可以返回schema信息:
{
get
{
// if a type and the select method have been
// specified, the schema can be refreshed
if (!String.IsNullOrEmpty(TypeName) && !String.IsNullOrEmpty(
SelectMethod))
{
return true;
}
else
{
return false;
}
}
}
为了实现RefreshSchema方法,我们必须扩展schema信息和产生SchemaRefreshed事件,如果数据源控件可以提供schema信息,schema信息将被在DesignerDataSouceView中的Schema属性中得到。然而,SchemaRefreshed事件不可是随时触发,只有当数据源返回一个不同的schema。为了说明这个的重要性,想想这个:如果数据源绑定到一个GridView上,当每一次RefreshSchema时间被触发,设计器将被询问是否产生Columns和data keys。因此,我们只关心在schema改变的时候触发SchemaRefreshed时间。我们用Designer state保存先前的schema,一旦RefreshSchema方法被调用,我们去检查schema是否改变,当改变的时候我们触发SchemaRefreshed事件。下面的代码涉及到RefreshSchema事件:
{
get
{
return DesignerState["DataSourceSchema"] as IDataSourceViewSchema;
}
set
{
DesignerState["DataSourceSchema"] = value;
}
}
public override void RefreshSchema(bool preferSilent)
{
// saves the old cursor
Cursor oldCursor = Cursor.Current;
try
{
// ignore data source events while refreshing the schema
SuppressDataSourceEvents();
try
{
Cursor.Current = Cursors.WaitCursor;
// gets the Type used in the DataSourceControl
Type type = GetType(Component.Site, TypeName);
// if we can't find the type, return
if (type == null)
{
return;
}
// get all the methods that can be used as the select method
MethodInfo[] methods =
type.GetMethods(BindingFlags.FlattenHierarchy |
BindingFlags.Static | BindingFlags.Instance |
BindingFlags.Public);
MethodInfo selectedMethod = null;
// iterates through the methods searching for the select method
foreach (MethodInfo method in methods)
{
// if the method is named as the selected method, select it
if (IsMatchingMethod(method, SelectMethod))
{
selectedMethod = method;
break;
}
}
// if the SelectMethod was found, save the type information
if (selectedMethod != null)
{
RefreshSchemaInternal(type, selectedMethod.Name,
selectedMethod.ReturnType, preferSilent);
}
}
finally
{
// restores the cursor
Cursor.Current = oldCursor;
}
}
finally
{
// resume data source events
ResumeDataSourceEvents();
}
}
internal void RefreshSchemaInternal(Type typeName,
string method, Type returnType, bool preferSilent)
{
// if all parameters are filled
if ((typeName != null) && (!String.IsNullOrEmpty(method)) &&
(returnType != null))
{
try
{
// gets the old schema
IDataSourceViewSchema oldSchema = DataSourceSchema;
// gets the schema of the return type
IDataSourceViewSchema[] typeSchemas =
new TypeSchema(returnType).GetViews();
// if we can't get schema information from the type, exit
if ((typeSchemas == null) || (typeSchemas.Length == 0))
{
DataSourceSchema = null;
return;
}
// get a view of the schema
IDataSourceViewSchema newSchema = typeSchemas[0];
// if the schema has changed, raise the schema refreshed event
if (!DataSourceDesigner.ViewSchemasEquivalent(
oldSchema, newSchema))
{
DataSourceSchema = newSchema;
OnSchemaRefreshed(EventArgs.Empty);
}
}
catch (Exception e)
{
if (!preferSilent)
{
ShowError(DataSourceComponent.Site,
"Cannot retrieve type schema for " +
returnType.FullName + ". " + e.Message);
}
}
}
}
你可以看到,我们得到MethodInfo从SelectMethod并且得到返回的type。暴露schema信息的困难工作已经由Framework的TypeSchema类完成了.DesignerSource view暴露了保存的schmea:
{
get
{
// if a type and the select method have been
// specified, the schema information is available
if (!String.IsNullOrEmpty(_owner.TypeName) && !String.IsNullOrEmpty(
_owner.SelectMethod))
{
return _owner.DataSourceSchema;
}
else
{
return null;
}
}
}
最后要澄清的事情是我们重载了PreFilterProperties方法在CustomDataSouceDesigner类中,这是为了更正如何让TypeName和SelectMethod属性工作。这是因为这些属性改变的时候在data souce和schema下的属性也会改变。因此,我们必须通告相关联的设计器。
{
base.PreFilterProperties(properties);
// filters the TypeName property
PropertyDescriptor typeNameProp =
(PropertyDescriptor)properties["TypeName"];
properties["TypeName"] = TypeDescriptor.CreateProperty(base.GetType(),
typeNameProp, new Attribute[0]);
// filters the SelectMethod property
PropertyDescriptor selectMethodProp =
(PropertyDescriptor)properties["SelectMethod"];
properties["SelectMethod"] =
TypeDescriptor.CreateProperty(base.GetType(),
selectMethodProp, new Attribute[0]);
}
public string TypeName
{
get
{
return DataSourceComponent.TypeName;
}
set
{
// if the type has changed
if (String.Compare(DataSourceComponent.TypeName, value, false) != 0)
{
DataSourceComponent.TypeName = value;
// notify to the associated designers that this
// component has changed
if (CanRefreshSchema)
{
RefreshSchema(true);
}
else
{
OnDataSourceChanged(EventArgs.Empty);
}
UpdateDesignTimeHtml();
}
}
}
public string SelectMethod
{
get
{
return DataSourceComponent.SelectMethod;
}
set
{
// if the select method has changed
if (String.Compare(DataSourceComponent.SelectMethod,
value, false) != 0)
{
DataSourceComponent.SelectMethod = value;
// notify to the associated designers that this
// component has changed
if (CanRefreshSchema && !_inWizard)
{
RefreshSchema(true);
}
else
{
OnDataSourceChanged(EventArgs.Empty);
}
UpdateDesignTimeHtml();
}
}
}
全部的源代码包括设计器和数据源控件都是可以在这里下载的。你可以看到,添加一个为data source控件的设计时支持并不是很可怕的事情,但是你必须写很多的代码,在这个例子中有1300多行,即使在一个简单的数据源控件,代码也很多,如果更复杂的数据源,你不得不写更多的代码。