Creating a custom DataSourceControl with full design time support






4.50/5 (10 votes)
Jan 3, 2007
8 min read

64923

984
An article on creating a DataSourceControl with full design time support
Introduction
This article shows how to create a custom DataSourceControl and how to add full design time support to it.
Background
This article assumes that you're familiar with DataSourceControls and that you know how design time infrastructure works. If this is not the case, take a look to the following articles.
For DataSourceControls:
- Data Source Controls - Under the hood (1/4)
- Data Source Controls - Under the hood (2/4)
- Data Source Controls - Under the hood (3/4)
- Data Source Controls - Under the hood (4/4)
For design time infrastrucutre:
- Introduction to designers
- ASP.NET designers. The ControlDesigner class
- ASP.NET designers. The DataSourceDesigner class
Creating the custom DataSourceControl
The data source we're going to code will be able to retrieve data but not modify it. It supports the Select operation only. It will be similar to ObjectDataSource, but only to retrieve data. It will have a TypeName property that will hold the name of a class and a SelectMethod property that will hold the method to call in that class. To avoid writing a lot of code, we'll call static methods only. We'll also have a collection of parameters to pass to the SelectMethod (SelectParameters). I'll explain the main tasks to perform when creating a DataSourceControl, but I won't explain in detail what a method or a property does. The code should have enough comments in the complex areas for you to be able to follow me.
The first thing to do when implementing a DataSourceControl is to choose how many DataSourceViews we're going to have and code the IDataSource related methods. In this sample, we'll have one view only:
public class CustomDataSource : DataSourceControl
{
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;
}
}
}
As the CustomDataSourceView is the class that does all of the job, the best approach is to store the properties in that class. However, we need to expose those properties in the CustomDataSource class to let the user modify them in the property grid. So, we need to add this to the CustomDataSource class:
[Category("Data"), DefaultValue("")]
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; }
}
And add this to the CustomDataSourceView class:
public class CustomDataSourceView : DataSourceView, IStateManager
{
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;
}
}
Note that when a property changes, the OnDataSourceViewChanged method is called to force a re-bind. Also note that the CustomDataSourceView class implements the IStateManager to support custom view state management. In this case, we use it to save SelectParameters. The state management in the CustomDataSource class is:
protected override void LoadViewState(object savedState)
{
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);
}
}
We use a pair to store the view state. The first element is used to store the parent's view state and the second element is used to store the view's view state. For CustomDataSourceView, the state management is:
bool IStateManager.IsTrackingViewState
{
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();
}
}
We need to evaluate SelectParameters on every request because if the parameters have changed, we have to rebind:
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);
}
}
protected virtual void UpdateParameterValues(object sender, EventArgs e)
{
SelectParameters.UpdateValues(Context, this);
}
The only thing left to do is the actual selection from CustomDataSourceView:
protected override IEnumerable ExecuteSelect(
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;
}
This code is far from production code. For example, there can be several methods with the same name as the SelectMethod, but with different parameters. The parameter conversion doesn't handle reference and generic types well. There isn't support for DataSet and DataTable types, as they don't implement IEnumerable. You'll also have to extract the underlying DataView to work with them. However, adding all of those "extra features" will make things harder to understand.
Now we are going to create a designer for our CustomDataSource control. The main tasks that have to be performed by DataSourceDesigner are:
- configuring the data source
- exposing schema information
Also, we have to expose at least one DesignerDataSourceView. A DataSource control exposes one or more DataSourceViews and a DataSourceDesigner exposes one or more DesignerDataSourceViews:
private static readonly string[] _views = { "DefaultView" };
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;
}
As you can see, the code is very similar to that used in the custom data source to expose the custom data source view. As our data source will only retrieve data, the default implementation of DesignerDataSourceView is enough for all CanXXX properties. In order to quickly configure our custom DataSource, we'll provide a GUI that will let us choose the TypeName and the SelectMethod using DropDownLists:

In order to be able to show the Configure Data Source dialog we need to override the CanConfigure property and implement the Configure method:
public override bool CanConfigure
{
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;
}
As the GUI will change several properties at a time, we have to create a transacted change in order to provide undo functionality. The form fills the first drop-down list with all available types using the type discovery service instead of reflection. Why? Because using reflection, we can only get all types of the compiled assemblies. However, we can add more types without having compiled the project. We can also have types that don't compile and the type discovery service will also show them. So, it is a lot better to use the type discovery service instead of reflection.
In the code, we haven't removed types that will probably not be candidates for our TypeName property -- i.e. generic types, interfaces -- in order to keep code as simple as possible:
private void DiscoverTypes()
{
// 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();
}
}
}
The TypeItem class is a class used to store types in the drop-down list. When a type is selected from the first drop-down list, the other drop-down list gets populated with the methods of the selected type:
private void FillMethods()
{
// 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();
}
}
To quickly get and set TypeName and SelectMethod from and to the form, we have defined those properties in the form as follows:
internal string TypeName
{
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;
}
}
Note that to simplify code when the SelectMethod property is set, the selected method from the drop-down list will be the first method with the same name as SelectMethod. No parameters are checked to simplify the code, but for production code you'll probably want to check that the parameters match.
In the FillMethods method, the type is obtained using the GetType method that used the resolution service. This is for the same reasons that we specified before for using the type discovery service. In order to simplify the code, we have not removed some methods that will certainly not be the proper method, like property getters and setters or abstract methods.
internal static Type GetType(IServiceProvider serviceProvider,
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);
}
When the user clicks the Accept button in the Configure data source form, the code that gets executed is:
private void bOK_Click(object sender, EventArgs e) { // 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); } }
We save the Type and the SelectMethod and refresh the schema. To provide schema information, we have to return true in the CanRefreshSchema method and we have to implement the RefreshSchema method. When we provide schema information, the controls can provide field pickers -- i.e. columns for a GridView -- and generate templates based on the schema information, i.e. a DataList bound to our data source control. However, we cannot return true for the CanRefreshSchema because we can return schema information only if the user has configured the data source:
public override bool CanRefreshSchemablic override bool CanRefreshSchema
{
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;
}
}
}
To implement the RefreshSchema method, we need to extract the schema information and generate the SchemaRefreshed event. If a data source control can provide schema information, the schema information will be retrieved from the property Schema from the underlying DesignerDataSourceView. However, the SchemaRefreshed event doesn't have to be raised every time, only if the data source returns a different schema. To see why this is important, think about this: if the data source is bound to a GridView, every time the RefreshSchema event is raised, the designer will ask if it has to regenerate the columns and the data keys. So, we're interested in raising the SchemaRefreshed event only when the schema changes. We use the designer state to store the previous schema. When the RefreshSchema method is called, we will check if the schema has changed, raising the SchemaRefreshed event only in that case. The code related to the RefreshSchema method is:
internal IDataSourceViewSchema DataSourceSchema
{
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);
}
}
}
}
As you can see, we get MethodInfo for SelectMethod and get the return type. All hard work to expose schema information is done by the framework helper class, TypeSchema. Take a look at the articles at the beginning for more information about the TypeSchema class. The DesignerDataSource view exposes the saved schema:
public override IDataSourceViewSchema Schema
{
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;
}
}
}
The last thing that needs clarifying is that we have overridden the PreFilterProperties method in the CustomDataSourceDesigner class in order to modify how the TypeName and SelectMethod properties work. This is because when any of those properties change, the underlying data source and schema will probably change. So, we have to notify it to the associated designers:
protected override void PreFilterProperties(IDictionary properties)
{
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();
}
}
}
The full source code of the designer and the data source control is available in the downloads for this article. As you can see, adding design time support to a data source control is not terribly complicated, but you have to write quite a bit of code -- 1300 lines in this sample -- even for simple data sources. The more complex your data source, the more code you will have to write.
Points of interest
The design time support covered for this data source is the most common scenario: the data source control doesn't render any HTML at run time and it only exposes a form to configure the data source. However, a data source control can also render HTML in some cases -- take a look at the PagerDataSource -- being not only a data provider, but also a data consumer. If you want to render HTML with your data source control, you have a lot of work to do as the framework doesn't have any base classes for data source controls that also render HTML.
History
- 01/03/2007 - Initial version
- 06/19/2007 - Article edited and moved to the main CodeProject.com article base