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:
For design time infrastrucutre:
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);
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);
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 (SelectMethod.Length == 0)
{
throw new InvalidOperationException(
_owner.ID + ": There isn't a SelectMethod defined");
}
arguments.RaiseUnsupportedCapabilitiesError(this);
IOrderedDictionary selParams =
SelectParameters.GetValues(System.Web.HttpContext.Current, _owner);
Type type = BuildManager.GetType(_typeName, false, true);
if (type == null)
{
throw new NotSupportedException(_owner.ID + ": TypeName not found!");
}
MethodInfo method = type.GetMethod(SelectMethod,
BindingFlags.Public | BindingFlags.Static);
if (method == null)
{
throw new InvalidOperationException(
_owner.ID + ": SelectMethod not found!");
}
ParameterInfo[] parameters = method.GetParameters();
IOrderedDictionary paramsAndValues =
new OrderedDictionary(parameters.Length);
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);
}
}
foreach (ParameterInfo currentParam in parameters)
{
string paramName = currentParam.Name;
object paramValue = selParams[paramName];
if (paramValue != null)
{
if (!currentParam.ParameterType.IsInstanceOfType(paramValue) &&
(paramValue is string))
{
TypeConverter converter =
TypeDescriptor.GetConverter(currentParam.ParameterType);
if (converter != null)
{
try
{
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 (paramsAndValues.Count > 0)
{
paramValues = new object[paramsAndValues.Count];
for (int i = 0; i < paramsAndValues.Count; i++)
{
paramValues[i] = paramsAndValues[i];
}
}
object returnValue = null;
try
{
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;
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;
}
IUIService UIService =
(IUIService) provider.GetService(typeof(IUIService));
if (UIService == null)
{
return false;
}
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()
{
ITypeDiscoveryService discovery = null;
if (_component.Site != null)
{
discovery =
(ITypeDiscoveryService)_component.Site.GetService(
typeof(ITypeDiscoveryService));
}
if (discovery != null)
{
Cursor previousCursor = Cursor.Current;
Cursor.Current = Cursors.WaitCursor;
try
{
ICollection types = discovery.GetTypes(typeof(object), true);
ddlTypes.BeginUpdate();
ddlTypes.Items.Clear();
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()
{
Cursor previousCursor = Cursor.Current;
Cursor.Current = Cursors.WaitCursor;
try
{
MethodInfo[] methods =
CustomDataSourceDesigner.GetType(_component.Site, TypeName).
GetMethods(BindingFlags.Public | BindingFlags.Static |
BindingFlags.Instance | BindingFlags.FlattenHierarchy);
ddlMethods.BeginUpdate();
ddlMethods.Items.Clear();
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
{
TypeItem selectedType = ddlTypes.SelectedItem as TypeItem;
if (selectedType != null)
{
return selectedType.Name;
}
else
{
return String.Empty;
}
}
set
{
foreach (TypeItem item in ddlTypes.Items)
{
if (String.Compare(item.Name, value, true) == 0)
{
ddlTypes.SelectedItem = item;
break;
}
}
}
}
internal string SelectMethod
{
get
{
string methodName = String.Empty;
if (MethodInfo != null)
{
methodName = MethodInfo.Name;
}
return methodName;
}
set
{
foreach (MethodItem item in ddlMethods.Items)
{
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)
{
ITypeResolutionService resolution =
(ITypeResolutionService)serviceProvider.
GetService(typeof(ITypeResolutionService));
if (resolution == null)
{
return null;
}
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 (String.Compare(TypeName, _component.TypeName, false) != 0)
{
TypeDescriptor.GetProperties(
_component)["TypeName"].SetValue(_component, TypeName);
}
if (String.Compare(SelectMethod, _component.SelectMethod, false) != 0)
{
TypeDescriptor.GetProperties(
_component)["SelectMethod"].SetValue(_component, SelectMethod);
}
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 (!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)
{
Cursor oldCursor = Cursor.Current;
try
{
SuppressDataSourceEvents();
try
{
Cursor.Current = Cursors.WaitCursor;
Type type = GetType(Component.Site, TypeName);
if (type == null)
{
return;
}
MethodInfo[] methods =
type.GetMethods(BindingFlags.FlattenHierarchy |
BindingFlags.Static | BindingFlags.Instance |
BindingFlags.Public);
MethodInfo selectedMethod = null;
foreach (MethodInfo method in methods)
{
if (IsMatchingMethod(method, SelectMethod))
{
selectedMethod = method;
break;
}
}
if (selectedMethod != null)
{
RefreshSchemaInternal(type, selectedMethod.Name,
selectedMethod.ReturnType, preferSilent);
}
}
finally
{
Cursor.Current = oldCursor;
}
}
finally
{
ResumeDataSourceEvents();
}
}
internal void RefreshSchemaInternal(Type typeName,
string method, Type returnType, bool preferSilent)
{
if ((typeName != null) && (!String.IsNullOrEmpty(method)) &&
(returnType != null))
{
try
{
IDataSourceViewSchema oldSchema = DataSourceSchema;
IDataSourceViewSchema[] typeSchemas =
new TypeSchema(returnType).GetViews();
if ((typeSchemas == null) || (typeSchemas.Length == 0))
{
DataSourceSchema = null;
return;
}
IDataSourceViewSchema newSchema = typeSchemas[0];
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 (!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);
PropertyDescriptor typeNameProp =
(PropertyDescriptor)properties["TypeName"];
properties["TypeName"] = TypeDescriptor.CreateProperty(base.GetType(),
typeNameProp, new Attribute[0]);
PropertyDescriptor selectMethodProp =
(PropertyDescriptor)properties["SelectMethod"];
properties["SelectMethod"] =
TypeDescriptor.CreateProperty(base.GetType(),
selectMethodProp, new Attribute[0]);
}
public string TypeName
{
get
{
return DataSourceComponent.TypeName;
}
set
{
if (String.Compare(DataSourceComponent.TypeName, value, false) != 0)
{
DataSourceComponent.TypeName = value;
if (CanRefreshSchema)
{
RefreshSchema(true);
}
else
{
OnDataSourceChanged(EventArgs.Empty);
}
UpdateDesignTimeHtml();
}
}
}
public string SelectMethod
{
get
{
return DataSourceComponent.SelectMethod;
}
set
{
if (String.Compare(DataSourceComponent.SelectMethod,
value, false) != 0)
{
DataSourceComponent.SelectMethod = value;
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