Blog Home  Home Feed your aggregator (RSS 2.0)  
A custom DataSourceControl sample - Manuel Abadia's ASP.NET stuff
 
# Friday, April 7, 2006

Before explaining the DataSourceDesigner class and all related classes/interfaces, we need a custom data source to be able to apply what we’ll learn about the DataSourceDesigner.

It’s difficult to make a decent sample of a custom data source for our purposes. If it’s too complex, it can difficult people to understand other things well, but if it’s too simple it’s difficult to make a designer that covers the main functionality of the DataSourceDesigner.

The data source we’re going to code will be able to retrieve data but not to modify it (it supports the Select operation only). It will be similar to the ObjectDataSource. 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).

If you don’t know the DataSourceControl and related classes/interfaces take a look at my January posts before continuing.

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 the job the best 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 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.

Note that the CustomDataSourceView class implements the IStateManager to support support custom view state management (in this case we use it to save the 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 the 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 the 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 select from the 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 have to extract the underlying DataView to work with them), etc but adding all those “extra features” will make things harder to understand.

I have created a sample website that uses this control with a GridView to display a list of products using 2 control parameters.

In next posts we’ll add design time support to this data source control.

Download source code: CustomDataSource.zip (6.18 KB)

Copyright © 2020 Manuel Abadia. All rights reserved.
DasBlog 'Portal' theme by Johnny Hughes.