Blog Home  Home Feed your aggregator (RSS 2.0)  
Creating a custom DataSourceDesigner - Manuel Abadia's ASP.NET stuff
 
# Wednesday, 19 July 2006

We will create a designer for the CustomDataSource we created here.

As we explained in a previous post about the DataSourceDesigner class, the main tasks that have to be performed by a 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 the one 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 the 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 dropdownlist 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 or we can 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 probably will not be candidates for our TypeName property (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 dropdownlist.

When a type is selected from the first dropdownlist, the other dropdownlist 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 the TypeName and the 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 dropdownlist will be the first method with the same name as the SelectMethod (no parameters are checked to simplify 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 (for the same reasons we specified before for using the type discovery service). In order to simplify the code we have not removed some methods that certainly will 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 can not return true for the CanRefreshSchema, because we can return schema information only if the user has configured the data source:

        public 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 everytime, 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, and 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 the MethodInfo for the SelectMethod and get the return type. All hard work to expose schema information is done by the framework helper class TypeSchema that was explained in the post about DataSourceDesigners. 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, 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 are in the files at the end of this post. 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 is your data source, the more code that you will have to write.

The design time support covered here 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.

CustomDataSourceDesigner.zip (13.5 KB)

CustomDataSourceDesigner_sampleweb.zip (26.77 KB)

Wednesday, 19 July 2006 10:27:16 (Romance Daylight Time, UTC+02:00)  #    Comments [2]   ASP.NET  |  Tracked by:
"DataSourceControls summary" (Manuel Abadia's ASP.NET stuff) [Trackback]
http://www.manuelabadia.com/blog/PermaLink,guid,7cef4a16-10ec-44e5-8f3c-36a977b8... [Pingback]
Copyright © 2018 Manuel Abadia. All rights reserved.
DasBlog 'Portal' theme by Johnny Hughes.