asp tutorials, asp.net tutorials, sample code, and Microsoft news from 15Seconds
Data Access  |   Troubleshooting  |   Security  |   Performance  |   ADSI  |   Upload  |   Email  |   Control Building  |   Component Building  |   Forms  |   XML  |   Web Services  |   ASP.NET  |   .NET Features  |   .NET 2.0  |   App Development  |   App Architecture  |   IIS  |   Wireless
 
Pioneering Active Server
 Power Search





Active News
15 Seconds Weekly Newsletter
• Complete Coverage
• Site Updates
• Upcoming Features

More Free Newsletters
Reference
News
Articles
Archive
Writers
Code Samples
Components
Tools
FAQ
Feedback
Books
Links
DL Archives
Community
Messageboard
List Servers
Mailing List
WebHosts
Consultants
Tech Jobs
15 Seconds
Home
Site Map
Press
Legal
Privacy Policy
internet.commerce














internet.com
IT
Developer
Internet News
Small Business
Personal Technology

Search internet.com
Advertise
Corporate Info
Newsletters
Tech Jobs
E-mail Offers

HardwareCentral
Compare products, prices, and stores at Hardware Central!

Automating Control Validation and ToolTips in Visual Studio 2005
By David Catherman
Rating: 4.3 out of 5
Rate this article


  • email this article to a colleague
  • suggest an article



    Visual Studio 2005 Windows Forms has some new extended provider components that allow ToolTips and validation error messages to be assigned to individual controls, but the process requires a fair amount of developer time in large projects. A better way is to automate the process to make the ToolTips and validation rules table driven so that non-developers (QA, Support, and even end users) can make changes easily and extend the Binding Source component to attach them to a form.

    Introduction

    Have you ever been very close to getting an application ready for release (you're running behind schedule of course and the project manager is putting the pressure on) while the quality assurance testers and documentation keep asking for minor (nit-picky?) changes to the tool tips and validation messages? Wouldn't it be nice to let them make the changes themselves without requiring the valuable time of a developer to make the changes? If you can relate, keep reading.

    This is a continuation of my series of articles on building a framework for developing Windows Forms (Smart Clients) applications using the new tools available in Visual Studio 2005. The series started with an article on using Data Sources and other RAD tools for building the data bound forms, how to build a Business Logic layer using Typed Datasets, and most recently, creating a user control to extend the Binding Navigator. In this article, the Binding Source component will be extended as a Custom Control with Extender Provider properties to allow the control validation rules and messages and the ToolTip messages to be entered and edited in a metadata table and applied to the form as it is loaded.

    Extender Providers

    Extender Providers are specialized components that extend a set of controls on the form by adding properties and functionality to each control. There are three new extender provider components available in VS2005: the ToolTip, Error Provider, and Help Provider.

    ToolTips

    ToolTips are the balloon type help messages that pop up when the user hovers the mouse pointer over a control or portion of form. When the ToolTips component is added to a form, a new property is added to all the controls on the form. What ever the developer enters into the property will be shown in the balloon help at run-time.

    Error Provider

    The error provider shows a flashing icon (usually red) next to each control when the Error Message property is set. If a control does not pass validation, the message is set and the user can read the message as a ToolTip when the mouse pointer hovers over the error icon.

    Help Provider

    The Help Provider allows the developer to link to a context-sensitive help message to each control on the form. This is a future enhancement to add to our component.

    While the new properties show in the property panel at design time, at run time, the properties must be manipulated using the get and set methods in the provider. For example, to set the ToolTip at runtime, use the SetToolTip() method of the ToolTip component.

    Custom Extender Provider

    We can easily build other Extender Providers to provide other properties and functionality for a set of controls on the form. Since the Error Provider component does not provide properties to store the validation information, we will make this component an extender provider to handle the validation rule and message properties for each control

    Four Levels of Data Validation

    1. Database Level Validation

    The back-end database server is responsible for maintaining data integrity. The design rules of table relationships are enforced as data is added or deleted to make sure there are no orphan records. Other rules maintained by the server engine include unique indexes, clustering, etc. When these rules are violated, the transaction will be rolled back and an error message returned to the calling program

    Other database rules may be enforced through the use of stored procedures and triggers in which case, the developer is responsible for communicating the error messages.

    2. Business Logic Validation

    The middle tier of an application is responsible to catch as many errors as possible before passing the data on to the database. Since the data objects have all the metadata information from the database, many of the rules such as data type and allow nulls (required fields) can be enforced at this level. Usually this validation happens as a row or record of data is saved. In a dataset, a partial class can be written to intercept the Row object and the RowChanging event to build custom rules for an Entity object.

    3. Control Validation

    Much of the typical validation can be handled at the control level. As the user finishes entering information into a control, the validation routine will check the accuracy of the data and notify the user of the problems. The user should not be able to leave the current record until the errors have been dealt with.

    4. Character Validation

    While Control validation handles the field after the data has been entered, sometimes it is nice to not allow the user to enter incorrect information in the first place. The Input Mask controls help in this area and even show a template for the user to follow in data entry. If more control is required, the TextChanged event can be handled to check input as each key is pressed.

    Regular Expressions

    This article will concentrate on the third type of validation--control level validation. A common method of encoding the validation rules is to use Regular Expressions. Although they can be cryptic to understand, Regular Expressions are an industry standard and there are many examples available for just about any possible validation rule. There are many good articles available on regular expressions available so I will not give a detail explanation here. All we need to know for now is that each control is provided a Validation Rule property that can be set in the form of a regular expression. If the data entered in the control does not match the rule, the string Validation Message property is sent to the Error Provider which will show the error icon next to the control.

    Control Validation Events

    VS2005 provides the Validating() and Validated() events for validation of controls. These events fire when the user leaves a control or when the Validate() method is called on the container control or form. By subscribing to the Validating event of each control, the value of the control can be tested and the cancel property of the CancelEventArgs can be set true to prevent the focus from leaving the control.

    While this is the standard way to validate controls, the problem is that the events fire on the leave of a control whether the data was changed or not. This article looks into a slightly different approach since the controls we are trying to validate are all bound controls. The Binding Source fires a Binding Complete event right after the Validating event when the user has made changes to the control. While this event does not have the Cancel property to stop the user from leaving the control, using the Error Provider to mark the errors is a little more graceful to the user.

    Metadata Table

    The first step toward our goal of assigning validation and ToolTip properties is to create a table to store the metadata. This could be just as well stored in XML or some other format, but since there is usually a database behind the application, we may as well use it. In Server Explorer, right click on the database and select New Table. In the Table Designer add the following fields (or use the create script in Appendix A):

    FieldName Data Type
    TableFieldID int
    TableName varchar(50)
    FieldName varchar(50)
    FieldDesc varchar(255)
    FieldType varchar(20)
    FieldLabel varchar(30)
    FieldShortLabel varchar(10)
    RequiredFlag bit
    ValidationType varchar(50)
    ValidationRule varchar(4000)
    ValidationMessage varchar(4000)
    ToolTip varchar(4000)
    HelpTopicID int
    SecurityID UniqueIdentifier

    Make sure the TableFieldID is set as the primary key and has an Identity property. Now save the new table with the name _TableFieldList. (I use the underscore prefix to separate the metadata tables from the application tables.) The additional fields of HelpTopicID and SecurityID will be used in future articles.

    I have created a stored procedure that will lookup into the system tables and populate the new table with a list of all tables and fields in the database (see uspTableFieldsUpdate in Appendix A). This may get a few extra or unneeded fields, but they can be ignored. The SP will call a function (udrConvertToLabel) default the Field Label to insert the space in the Field Name where needed. See the download code for details on this code. Execute this stored procedure to fill the table with metadata about the current database.

    Editing the Metadata

    With the table now populated with all the fields in the user tables, we need a way for the product assurance (or users) to edit the extended fields. Using my DotNet2005 framework, this process begins by creating a DataSet object to wrap our table with strong typing and generate the necessary data access logic.

    To make a 3-tiered application, the dataset should be put in a business logic project, but for article simplicity we will keep all of the application items in one project and split them out later. Before creating a new project, I recommend first creating a blank solution to keep the location of the solution files logical. Then add a new Windows project to the solution and give it the name Win.

    To define the DataSet, you can add a new DataSet item to the project and drag the tables from the Server Explorer or you can use the Data Sources wizard to create the DataSet. Give the DataSet the name TableFieldsDataSet and add the table _TableFields to the designer screen. (If this is all new to you, please see my previous article.) Add a new query to the table adapter to fill the table filtered by TableName.

    We also need a table that shows a list of tables in the database. Drag the _TableFieldsList table onto the designer again and rename it to TableList. Configure the adapter to just build a list of tables by changing the SQL to:

    SELECT TableName
    FROM _TableFieldList
    GROUP BY TableName

    Add a relation between the two tables by dragging from the TableName field in one table to the TableName field in the other table. The wizard will create the join line between the tables. The designer should now look like this:

    In order to keep the tiers of the application separate, we need to wrap the data access logic in the dataset. Right click on the designer screen and select View Code to open the code window. Inside of the partial class stub for the TableFieldsDataSet, add the following code:

    partial class TableFieldsDataSet
    {
        static TableFieldsDataSetTableAdapters._TableFieldListTableAdapter taTableFields =
            new Win.TableFieldsDataSetTableAdapters._TableFieldListTableAdapter();
        partial class _TableFieldListDataTable
        {
            public void Fill()
            {
                taTableFields.Fill(this);
            }
            public void FillByTableName(string TableName)
            {
                taTableFields.FillByTableName(this, TableName);
            }
            public void Update()
            {
                taTableFields.Update(this);
            }
        }

        static TableFieldsDataSetTableAdapters.TableListTableAdapter taTableList =
            new Win.TableFieldsDataSetTableAdapters.TableListTableAdapter();
        partial class TableListDataTable
        {
            public void Fill()
            {
                taTableList.Fill(this);
            }
        }
    }

    With the dataset created, it is very easy to create a new form to edit the metadata. Add a new Windows Form to the project and name it TableMetadata (or rename the default Form1 that was created by the IDE.)

    To add bound controls to the form, open the Data Source panel (from the Data menu). The TableFieldsDataSet should be shown. (If you placed the dataset in the business project, you will need to create an Object DataSource to reference the dataset.) Select the table _TableFieldList and drop down the render type combo box next to it and select the Detail render type instead of the DataGridView. Drag and drop the table onto the form. The wizard now creates an instance of the DataSet, a Binding Source pointing to the table, a Binding Navigator to move between records and a label and a control for each selected field in the table.

    We need some navigation so the user can easily select the table and field desired. For the table, add a combo box at the top left of the form and also put a label in front of it. Drag the TableList table from the Data Source panel and drop it on the Combo Box to set up the data source and display field information. Now drag the TableName field in the TableList table in the Data Sources panel and drop it on the Combo Box to set the binding properties. Now the combo box will show a list of the tables and when the user selects a table, the position of the binding source will move to that record which will allow the form to display a parent-children relationship between the table and the fields in that table.

    In the Data Sources panel, select the _TableFieldsList table, drop down the list beside it and choose the rendering method to be DataGridView. Drag the table to the form again to create a grid of all the fields. It should use the same binding source as the Detail controls. From the Smart Tag of the grid, select Edit Columns and remove all the columns except the FieldName. Size the grid to the size of the one column and move it to the left of the Detail controls.

    To make the master-detail relation work, select the _TableFieldListBindingSource and change the DataSource property to the tableListBindingSource and the DataMember property to the relation TableList__TableFieldList. Now when the form is run, the DataGridView should show a list of fields for the selected table and which ever field is selected in the grid will show in the Detail controls.

    Before the form will run, double click on the form header and add the following code to the Form_Load event handler to call the dataset methods to fill the tables:

        private void TableMetadata_Load(object sender, EventArgs e)
        {
            tableFieldsDataSet._TableFieldList.Fill();
            tableFieldsDataSet.TableList.Fill();
        }

    If you want to be able to save the changes, you need to add code to the save button. Enable the button in the navigation bar with the diskette and double click it to get the code stub and enter the following code (or you could add an extended Binding Navigator from the previous article):

        private void _TableFieldListBindingNavigatorSaveItem_Click(object sender, EventArgs e)
        {
            this.Validate();
            this._TableFieldListBindingSource.EndEdit();
            tableFieldsDataSet._TableFieldList.Update();
        }

    After rearranging the controls in a more logical format, you should end up with a form like this:

    To test the regular expressions, add label and a couple textbox controls to the form named TestRuleTextBox and PassFailTextBox. Select the TestRule textbox and from the events section of the properties panel (click the lightening bolt), add an event handler for the Leave event that will evaluate the regular expression and set the text of the next textbox depending on the outcome:

        private void TestRuleTextBox_Leave(object sender, EventArgs e)
        {
            System.Text.RegularExpressions.Regex regex;
            regex = new System.Text.RegularExpressions.Regex(@validationRuleTextBox.Text);
            if (regex.IsMatch(TestRuleTextBox.Text))
                PassFailTextBox.Text = "Passes";
            else
                PassFailTextBox.Text = "Failed";
            PassFailTextBox.Focus();
        }

    Make sure the PassFail textbox is next in the Tab Order of the form and add an event handler for the it also to reset the message:

        private void PassFailTextBox_Leave(object sender, EventArgs e)
        {
            PassFailTextBox.Text = "";
        }

    Set this form as the default form for the project and try running the project to test the form. Try entering several validations and ToolTips for the fields in the _TableFields table.

    Extending the Binding Source

    Now that metadata has been defined, how does it get applied to each form? We need to create a custom component that will lookup the metadata and set the appropriate properties. The controls do not have any properties for all of the information, so our custom component will also need to be an extender provider that adds the appropriate properties to each control on the form.

    Since the data is defined by table name, we need some way to discover what table a form is based on. The bound table is contained in the Binding Source component so we could create a property for the developer to set to define the binding source we are using. An even better solution is to make our new component inherit from the Binding Source component and extend it to include the extra properties and methods.

    In my previous article on Extending the Binding Navigator, we overcame some deficiencies in the Binding Source component by providing an IsDataDirty flag property and a method to discover the table behind the binding source and make it available via another property. We can now take the properties and methods out of the Binding Navigator and extend the Binding Source component instead. If the Binding Navigator uses our new extended binding source, it can easily have access to the properties as needed.

    Creating a Custom Control

    In the Solution Explorer panel, add another item to our project, a Custom Control named exBindingSource. If you use this in a production application, you should create another Windows Control Library type of project to the solution and put all the user and custom controls in it so you can use the assembly in other applications. But for the sake of simplicity in the article, we will add it to the one project. We can always move it to another project later with only the complication of manually changing the namespace.

    Note: In C#, this opens to the design surface. In VB, it opens to the code page and you must double click on the new class in Solution explorer (or right click, Open) to get to the design surface.

    On the design surface for our new control, we need several components. From the toolbox, add a TableFieldsDataSet, ToolTip, ErrorProvider, and a ContextMenuStrip (accept the default name of each with a 1 on the end). We will be referring to each of these in code.

    Switch to the code view of the new component. By default, the control inherits from Control, but we need to change this to inherit from BindingSource. We also need to implement the IExtenderProvider interface to add the extended properties for validation rule and validation message to each control.

        [ProvideProperty("ValidationRule", typeof(String))]
        [ProvideProperty("ValidationMessage", typeof(String))]
        public partial class exBindingSource : BindingSource, IExtenderProvider

    Note: Here is another difference between C# and VB--in VB, you will need to open the exBindingSource.Designer.vb generated code file(click Show All Files to unhide) and remove the inheritance from Control.

    Implementing the Extender Provider Interface

    In the above code, the IExtenderProvider interface was invoked and two properties were defined in the attributes that will be added to each control. To implement the Extender Provider, we need to provide a Get and Set method for each of the properties and a CanExtend method passing the control as a parameter.

    To make extended properties available for controls, we must provide a Get and Set method, prefixed by the name of the property, for example SetValidationRule). We must also provide an array to store the properties of each control, for which we will use a dictionary object indexed on the control reference. When the validation rule and message are set for each control, the data will be stored in a dictionary collection and then retrieved by the Get method.

    #region "Extender methods"
        //collections for tracking validation info
        private Dictionary<Control, string> _validationRule =
            new Dictionary<Control, string>();
        private Dictionary<Control, string> _validationMessage =
            new Dictionary<Control, string>();

        public string GetValidationRule(Control ctl)
        {
            if (_validationRule.ContainsKey(ctl))
                return _validationRule[ctl];
            else
                return "";
        }

        public void SetValidationRule(Control ctl, string value)
        {
            //add value to dictionary for control
            _validationRule.Add(ctl, value);
        }

        public string GetValidationMessage(Control ctl)
        {
            if (_validationMessage.ContainsKey(ctl))
                return _validationMessage[ctl];
            else
                return "";
        }

        public void SetValidationMessage(Control ctl, string value)
        {
            _validationMessage.Add(ctl, value);
        }

        public bool CanExtend(object obj)
        {
            Control ctl = obj as Control;
            if (ctl != null)
            {
                //is the control bound to the current bindingsource
                if (ctl.DataBindings != null && ctl.DataBindings.Count > 0 &&
                    ctl.DataBindings[0].DataSource.GetType() == typeof(exBindingSource))
                    if (ctl.DataBindings[0].DataSource == this)
                        return true;
            }
            return false;
        }
    #endregion

    In the CanExtend method, we look to see if the control is bound to this binding source before allowing the properties to be added. When VS formats the screen, it will execute this method and add the properties for the appropriate controls. The properties will be listed as "ValidationRule on exBindingSource1" in case you have multiple extended components on a form.

    Component Initialization and Properties

    The default OnPaint event that was added by Visual Studio can be removed. The constructor needs to be expanded to handle the overloads, call the constructor of the base BindingSource and then call our Initialize method.

        public exBindingSource()
            : base()
        {
            Initialize();
        }
        public exBindingSource(IContainer container)
            : base(container)
        {
            Initialize();
        }
        public exBindingSource(object dataSource, string dataMember)
            : base(dataSource, dataMember)
        {
            Initialize();
        }

    Next comes the properties needed for our extended binding source component. The DataTable and the DataSet are derived from the DataSource and DataMember properties of the binding source and made available. The IsDataDirty and HasErrors flags track when edits have been made to the current record and when some data does not pass validation. Then there are also properties that track if ToolTips and Validation are enabled and whether the user is allowed to edit the Tooltips.

        //local variables
        string FirstCtl;        //first control in the binding complete cycle
        System.Text.RegularExpressions.Regex regex;
    #region "BindingSource Properties"
        private bool _hasErrorsFlag;              //Validation errors present
        private bool _isDataDirty;                //have there been edits to the data
        private DataTable _dataTable;             //reference to the table use for binding
        private DataSet _dataSet;                 //reference to the dataset of the table
        private bool _enableTooltips;             //Load the tooltips table
        private bool _enableValidation;           //load the validation routine
        private bool _enableTooltipEdit = false;  //flag to enable tooltip edit while loading
        private Form _parentForm;                 //reference to the parent form
        //collection of the controls for this binding source
        public System.Collections.ObjectModel.Collection<Control> ControlCollection
            = new System.Collections.ObjectModel.Collection<Control>();

        /// <summary>
        /// get the data table (read-only, set through binding source properties)
        /// </summary>
        [Browsable(true)]
        public DataTable DataTable
        {
            get { return _dataTable; }
        }

        /// <summary>
        /// get a reference to the dataset - set through Binding Source
        /// </summary>
        [Browsable(true)]
        public DataSet DataSet
        {
            get { return _dataSet; }
        }

        /// <summary>
        /// Flag for Data Validation fails
        /// </summary>
        public bool HasErrorsFlag
        {
            get { return _hasErrorsFlag; }
            set
            {
                if (value == false) //if reset, check other controls first
                {
                    //loop through the controls and check the error message on each
                    foreach (Control ctl in ControlCollection)
                    {
                        if (errorProvider1.GetError(ctl).Length > 0)
                            return; //if any one is set, HasErrors stays true
                    }
                }
                _hasErrorsFlag = value;
            }
        }

        /// <summary>
        /// Flag for data has been edited
        /// </summary>
        public bool IsDataDirty
        {
            get { return _isDataDirty; }
            set
            {
                _isDataDirty = value;
                if (_isDataDirty == true)
                {
                //Update the parent isdatadirty if it is a exBindingSource also
                    exBindingSource bs = (this.DataSource as exBindingSource);
                    if (bs != null)
                        bs.IsDataDirty = value;
                }
            }
        }

         /// <summary>
        /// Flag to process tooltips for bound control
        /// </summary>
        [Browsable(true), Category("Design")]
        public bool EnableTooltips
        {
            get { return _enableTooltips; }
            set { _enableTooltips = value; }
        }

        /// <summary>
        /// Flag to process validation for bound controls
        /// </summary>
        [Browsable(true), Category("Design")]
        public bool EnableValidation
        {
            get { return _enableValidation; }
            set { _enableValidation = value;}
        }

        /// <summary>
        /// Allow users to edit tooltips
        /// </summary>
        public bool EnableTooltipEdit
        {
            get { return _enableTooltipEdit; }
            set { _enableTooltipEdit = value; }
        }


        #endregion //property

    Now, let's get down to the methods needed to make this component work. This Initialize method is called from the constructor, first calls the InitializeComponent from the designer generated code that instantiates our components. Then we need to subscribe to a couple events of the binding source (explained below).

        private void Initialize()
        {
            InitializeComponent();
            //add the event handlers
            this.DataSourceChanged += new System.EventHandler(bs_DataSourceChanged);
            this.DataMemberChanged += new System.EventHandler(bs_DataSourceChanged);
            this.BindingComplete += new BindingCompleteEventHandler(bs_BindingComplete);
            //if base properties already set, process these properties now
            if (this.DataSet != null)
                bs_DataSourceChanged(this, new EventArgs());
        }

    The first order of business is to determine the data table behind the binding source. When the component initializes, usually the dataset has not been set yet, so we need to subscribe to the change events of the Data Source and Data Member properties and send the program to our code to derive the table. But if the DataSource has been defined, execute the code now.

    Deriving the Data Table

    Deriving the table is not as straight forward as it seems. The BindingSource property of a binding source can be set either to a dataset or to another binding source. If it is a dataset, the table name will be the DataMember property and all we have to do is get a reference to the table in the dataset.

    If the data source is another binding source, it means there is a master-detail relationship. The dataset can be found by walking up the chain until the parent DataSource property is a dataset. Then the DataMember property lists the relation between the parent table and the child table. To get the correct table reference, find the relation in the set of relations for the dataset and take the child table reference.

    //when the data source or data member is set, subscribe to the events
    private void bs_DataSourceChanged(object sender, EventArgs e)
    {
        if (this.DataSource == null | this.DataMember == "") return;
        //if child bindingsource, subscribe to the parent DataSource Changed event
        if ((this.DataSource as BindingSource) != null)
            (this.DataSource as BindingSource).DataSourceChanged
                += new EventHandler(bs_DataSourceChanged);
        //if child bindingsource of a exBindingSource, set parent property
        if ((this.DataSource as exBindingSource) != null)
        {
            this.ParentBindingSource = (this.DataSource as exBindingSource);
        }
        object obj = this.DataSource;
        //if datasource is another binding source, loop until the parent dataset is found.
        while ((obj as BindingSource) != null)
            obj = (obj as BindingSource).DataSource;
        //make sure obj is now a dataset
        if ((obj as DataSet) != null)
        {
            _dataSet = (DataSet)obj;
            //is the DataMember a table or a relation
            _dataTable = (_dataSet.Tables[this.DataMember] as DataTable);
            if (_dataTable == null)
                //it must be a relation instead of a table
                _dataTable = _dataSet.Relations[this.DataMember].ChildTable as DataTable;
        }
    }

    Setting up Validation

    Back to our Initialize method, the next thing we need to accomplish is to set up the validation properties of the bound controls. I hope that in a future version, Microsoft will make accessible the collection of components that are bound to the binding source. It may be buried there somewhere, but I have not been able to find it yet. So we will write some code to collect this information ourselves using the generic collection named ControlCollection as defined in the properties section.

    The binding source does have an event called BindingComplete that fires every time the binding source interfaces with a control, both writing the data to the control and accepting user input back from the control. By subscribing to this event, we can build the needed information and in the process, setup the controls for validation and tooltips.

    When each record is processed, the binding source will cycle through all the bound controls, updating them with the correct information. By intercepting the BindingComplete event for the first cycle, we will be able to set up each control. The bs_BindingCompleteInitial method handles the event to accomplish the setup.

    //Inital pass through each control once --setup validation message
    private void bs_BindingCompleteInitial(object sender, BindingCompleteEventArgs e)
    {
        if (FirstCtl == null) //we are now on the first control in the first cycle
        {
            FirstCtl = e.Binding.BindingMemberInfo.BindingMember;
            if (tableFieldsDataSet1._TableFieldList == null)
            {
                //fill the metadata for this table
                tableFieldsDataSet1._TableFieldList.FillByTableName(_dataTable.TableName);
            }
            //get a reference to the parent form
            _parentForm = (e.Binding.Control.Parent as ContainerControl).ParentForm;
            if (_enableTooltipEdit) //add context menu to edit tooltips
                _parentForm.ContextMenuStrip = contextMenuStrip1;
        }
        //capture the first column of Binding source list to know when list repeats
        //if back to first control then finished initial setup
        else if (FirstCtl == e.Binding.BindingMemberInfo.BindingMember)
        {
            //extract the event handler for initial setup
            this.BindingComplete -= new BindingCompleteEventHandler(bs_BindingCompleteInitial);
            return;
        }
        //still in first loop through the controls, add the control name to the hash list
        ControlCollection.Add(e.Binding.Control);
        if (_enableTooltips | _enableValidation)
        {
            //lookup control in table and set error message
            DataRow[] rows = tableFieldsDataSet1._TableFieldList.Select("TableName='" +
                _dataTable.TableName + "' And FieldName='"
                + e.Binding.BindingMemberInfo.BindingField + "'");
            if (rows.Length > 0) //was the field found the in Metadata table
            {
                if (_enableValidation & rows[0]["ValidationRule"].ToString().Length > 0)
                {
                    //put the validation info in the extended properties
                    this.SetValidationRule(e.Binding.Control, rows[0]["ValidationRule"].ToString());
                    this.SetValidationMessage(e.Binding.Control,
                        rows[0]["ValidationMessage"].ToString());
                }
                if (_enableTooltips & rows[0]["ToolTip"].ToString().Length > 0)
                {
                    //set the tooltip for the control
                    toolTip1.SetToolTip(e.Binding.Control, rows[0]["ToolTip"].ToString());
                }
            }
        }
    }

    The first section of the code is when we hit the first control in the collection. To determine when we have gone through a complete cycle, capture the name of the first control in a local variable. This is also a good place to fill the data table containing our metadata that we will be using. This is also a good place get a reference to the form we are on. Getting the parent form is very hard from a component like the binding source. Now that we have a reference to a control, the parent containing control always has a reference to the form. Once we have the form reference, we can set up the context menu that will allow editing of the ToolTips.

    The second section covers when we have cycled through all the controls and are back to the first control on our second cycle. At this point, we are finished setting up all the controls and can unsubscribe to the binding complete event.

    The third section handles each control and adds it to the collection, gets the metadata for the control from the dataset, and sets the extended properties as necessary.

    Validating Controls

    Back to the Initialize method, the last step is to subscribe to the binding complete event again--this time to handle the validation of the control after the user has entered or edited the data. The bs_BindingComplete method catches those events where the response is coming back from the control to the dataset. This allows our validation routine to only execute when something has been edited as apposed to the most common method of subscribing to the Validating event of each control which fires every time the focus leaves a control whether data was changed or not.

        //handler for Binding Complete event of the binding source
        private void bs_BindingComplete(object sender, BindingCompleteEventArgs e)
        {
            //is the direction coming back from the control
            if (e.BindingCompleteContext == BindingCompleteContext.DataSourceUpdate)
            {
                if (e.BindingCompleteState == BindingCompleteState.Success
                    && !e.Binding.Control.BindingContext.IsReadOnly)
                {
                    IsDataDirty = true;
                    if (_enableValidation) //control validation
                    {
                        string msg = ValidateControl(e.Binding.Control);
                        //set the error icon or reset if msg is empty
                        errorProvider1.SetError(e.Binding.Control, msg);
                        //set the flag -- use getter to pass up chain
                        this.HasErrorsFlag = (msg.Length > 0);
                    }
                }
            }
        }

    The method sets the IsDataDirty flag (used by the Binding Navigator to know when to save data) and if validation is being used, validates the control. The ValidateControl method returns exactly the information needed to pass on to the SetError method of the error provider component--if the control is valid, it returns a empty string, otherwise it returns the error message for the control. When a non-empty message is sent to the error provider, it shows an error icon next to the control which blinks for a few seconds and then remains solid until the message is set to an empty string. (All of these render characteristics can be altered by changing a few properties.) Lastly, the HasError flag needs to be set or reset accordingly.

    The Error Provider component SetError method takes a string parameter that if not empty, turns on the error icon with the error message in a ToolTip for the icon. Calling the method with an empty string resets the error and the icon is made invisible. The ValidateControl method returns the same information needed to pass to the SetError method--either an error message if the control is not valid or an empty string if it does pass. The code to validate each control uses the extended properties placed in the control on setup.

        //returns a string that is empty if passes and has message if not valid
        public String ValidateControl(Control ctl)
        {
            string msg = ""; //return empty string if validation passes
            //is there a validation for this control
            string rule = this.GetValidationRule(ctl);
            if (rule.Length > 0)
            {
                regex = new System.Text.RegularExpressions.Regex(rule);
                //does the control violate the regular expression match
                if (!regex.IsMatch(ctl.Text))
                {
                    //lookup the message
                    msg = this.GetValidationMessage(ctl);
                    //add an event handler for the Text Changed event of the control
                    ctl.TextChanged += new EventHandler(ctl_TextChanged);
                }
                else //control does pass validation
                {
                    //remove the Text Changed event handler if validation passes
                    ctl.TextChanged -= new EventHandler(ctl_TextChanged);
                }
            }
            return msg;
        }

    Now here is a nifty little feature: if a control fails validation, the error icon is removed as soon as the user corrects the problem without waiting for the focus to move out of the control. When the error is set, we subscribe to the Text Changed event of the control which fires on every keystroke and reprocess the validation. As soon as the control passes (the last else in the code), unsubscribe to the event and return an empty string.

        /// <summary>
        ///event handler for the Text Changed event of a control
        /// which is subscribed to when a valadition error is noted.
        /// and removed when the error is fixed
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        public void ctl_TextChanged(object sender, EventArgs e)
        {
            errorProvider1.SetError((sender as Control), ValidateControl(sender as Control));
        }

    One last function needed is to force validation on all of the controls. This method cycles through each control in the collection and calls the validate routine for each. This method can be called if a record needs to be validated regardless whether the user makes changes (check for bad saved data).

        /// <summary>
        /// Validate all controls bound to the binding source
        /// </summary>
        public void ValidateAll()
        {
            //loop through each control and force validation
            foreach (Control ctl in ControlCollection)
            {
                string msg = ValidateControl(ctl);
                //set the error icon or reset if msg is empty
                errorProvider1.SetError(ctl, msg.Trim());
                //set the flag -- use getter to pass up chain
                this.HasErrorsFlag = (msg.Length > 0);
            }
        }

    Editing ToolTips

    The ToolTips were setup at the same time as the validation rules. Just setting the tooltip on the control will allow it to function at runtime. When the user hovers the mouse over a control, the message will appear in a balloon type popup next to the mouse. What would be nice to have is the capability to edit these tooltips at runtime.

    Remember we added a Context Menu Strip to the component. By programming this context menu, the user will be able to right click on the form and select the Enable ToolTip Editing menu item. Back on the Validation setup, we got a reference to the current form and set the context menu to our strip. Now when the user right clicks any background part of the form, the menu will be available.

    When the user selects this menu to enable editing, the following code will convert the context menu to read "Edit ToolTip" and set the menu to be the context menu for each control in the collection and remove it from the form. So when the user right clicks on any of the controls, there will be a context menu to edit the ToolTip text. For convenience, I am using the InputBox from the VisualBasic namespace, but you could just as easy open a dialog form instead. (Note: to use this you will need to add a reference to Microsoft.VisualBasic.)

    #region "tooltip methods"
        /// <summary>
        /// The menu acts as a toggle between enabling Tooltip edits and editing the tooltip
        /// by the wording of the menu item.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        public void EditToolTip_Click(object sender, EventArgs e)
        {
            //if edits are allowed
            if (_parentForm != null & _enableTooltipEdit)
            {
                //is the menu in enable mode (as apposed to edit mode)
                if (contextMenuStrip1.Text == "Enable ToolTip Edit")
                {
                    //initial process of all controls
                    contextMenuStrip1.Text = " Edit ToolTip";
                    _parentForm.ContextMenuStrip = null;
                    //set the context menu for each control
                    foreach (Control ctl in ControlCollection)
                        ctl.ContextMenuStrip = contextMenuStrip1;
                }
                else //menu is in edit mode
                {
                    //edit the tooltip for the selected control
                    string oldTip = "";
                    //get the field name from bindings (assumes first one if multiple bindings)
                    string fieldName = contextMenuStrip1.SourceControl.DataBindings[0].BindingMemberInfo.BindingField;
                    if (fieldName == "") return;
                    DataRow[] rows = tableFieldsDataSet1._TableFieldList.Select("TableName = '"
                        + _dataTable.TableName + "' AND FieldName='" + fieldName + "'");
                    if (rows.Length > 0)
                        oldTip = rows[0]["ToolTip"].ToString();
                    string tip = Microsoft.VisualBasic.Interaction.InputBox("Enter new Tooltip"
                        , "Edit Tooltip", oldTip, 0, 0);
                    if (tip.Length > 0 & tip != oldTip)
                    {
                        //edit the data in the table
                        if (rows.Length > 0)
                        {
                            //save new tip to the table
                            rows[0]["ToolTip"] = tip;
                            rows[0].EndEdit();
                        }
                        else
                        {
                            //add a new row in the table
                            TableFieldsDataSet._TableFieldListRow newRow =
                                tableFieldsDataSet1._TableFieldList.New_TableFieldListRow();
                            newRow.TableName = _dataTable.TableName;
                            newRow.FieldName = fieldName;
                            newRow.ToolTip = tip;
                            newRow.EndEdit();
                            tableFieldsDataSet1._TableFieldList.Rows.Add(newRow);
                        }
                        tableFieldsDataSet1._TableFieldList.Update();
                        //apply the new tooltip
                        toolTip1.SetToolTip(contextMenuStrip1.SourceControl, tip);
                    }
                }
            }
        }
    #endregion //tooltip methods

    When the user edits the ToolTip message, find the record in the Metadata table and make the change or add the record if it does not exist. Save the changes back to the database.

    Testing the Component

    Now we need a form to test the new component. Since we already have a form in the project to edit the metadata, let's use it as our test form.

    The form already has a binding source called _TableFieldListBindingSource. Rather than recreating all the bindings, let's convert it to our version of the binding source. In Solution Explorer, expand the "+" next to the TabelMetadata.cs form (you may need to click the Show All Files button at the top of Solution Explorer), and open the TableMetadata.Designer.cs file of generated code. Find the declaration of the binding source line of code:

        private System.Windows.Forms.BindingSource _TableFieldListBindingSource;

    And change it to use our new extended binding source.

        private Win.exBindingSource _TableFieldListBindingSource;

    Also find the line of code that instantiates the binding source:

        this._TableFieldListBindingSource = new System.Windows.Forms.BindingSource(this.components);

    And change it also.

        this._TableFieldListBindingSource = new Win.exBindingSource(this.components);

    Now build the Win project and make sure everything was converted correctly. If you switch back to the visual form designer, you will see that the icon on the Binding Source has changed to a generic component icon.

    Select the _TableFieldListBindingSource and in the properties set the EnableToolTips, EnableValidation, and the EnableToolTipEdit flag all to true. Now run the form and select the _TableFieldsList table. (If you did not execute the stored procedure built ealier, the metadata table may not have been populated.)

    Select the ShortLabel field and edit the following properties:

    Validation Rule ^\w{0,10}$
    Validation Message alphanumeric up to 10 chars
    ToolTip alphanumeric up to 10 chars

    Be sure and click the Save icon. You will need to stop debugging and start the form again to see the effects. Hover over the ShortLabel control for a second and the ToolTip will appear.

    Try entering eleven characters into the Short Label control and then tab out of the field.

    Conclusion

    The Binding Source component in Visual Studio 2005 has vastly improved data bound Windows forms, but even so, it can still be improved. By extending the component and making use of the Binding Complete event, we can implement an automated validation and ToolTip system based on metadata that can be configured by non-developers.

    Appendix 1

    Script to create MetaData table:

    IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[$TableFieldList]') AND type in (N'U'))
    DROP TABLE [dbo].[$TableFieldList]
    BEGIN
    CREATE TABLE [dbo].[_TableFieldList](
        [TableFieldID] [int] IDENTITY(1,1) NOT NULL,
        [TableName] [varchar](50) NULL,
        [FieldName] [varchar](50) NULL,
        [FieldDesc] [varchar](255) NULL,
        [FieldType] [varchar](20) NULL,
        [FieldLabel] [varchar](30) NULL,
        [FieldShortLabel] [varchar](10) NULL,
        [RequiredFlag] [bit] NULL,
        [ValidationType] [varchar](50) NULL,
        [ValidationRule] [varchar](4000) NULL,
        [ValidationMessage] [varchar](4000) NULL,
        [ToolTip] [varchar](4000) NULL,
    CONSTRAINT [PK_$TableFieldList] PRIMARY KEY CLUSTERED
    ( [TableFieldID] ASC) WITH (IGNORE_DUP_KEY = OFF) ON [PRIMARY]
    ) ON [PRIMARY]
    END
    GO

    Script for uspTableFieldsUpdate Stored Procedure

    CREATE PROCEDURE dbo.uspTableFieldsUpdate
    AS
    BEGIN
        With vTableFieldFromSys (TableName, FieldName, Type, Description, RequiredField)
        AS (
            SELECT
                TOP (100) PERCENT sys.objects.name AS TableName,
                sys.columns.name AS FieldName,
                sys.systypes.name + CASE WHEN max_length <> length THEN '(' + CONVERT(varchar, Max_length) + ')' ELSE '' END AS Type,
                CONVERT(varchar(255), Descr.value) AS Description,
                CASE WHEN sys.columns.is_nullable = 0 THEN 1 ELSE 0 END AS RequiredFlag
            FROM
                sys.objects INNER JOIN
                sys.columns ON sys.objects.object_id = sys.columns.object_id INNER JOIN
                sys.systypes ON sys.columns.system_type_id = sys.systypes.xtype LEFT OUTER JOIN
                    (SELECT major_id, minor_id, value
                        FROM sys.extended_properties
                        WHERE (name = 'MS_Description')) AS Descr ON
                    sys.columns.column_id = Descr.minor_id AND sys.columns.object_id = Descr.major_id
            WHERE (sys.objects.type = 'U')
            ORDER BY TableName, sys.columns.column_id
            )

    --Insert
    INSERT INTO [_TableFieldList]
            (TableName, FieldName, FieldType, FieldDesc, RequiredFlag,FieldLabel)
    SELECT  vTableFieldsFromSys.TableName, vTableFieldsFromSys.FieldName, vTableFieldsFromSys.Type,
            vTableFieldsFromSys.Description, vTableFieldsFromSys.RequiredFlag,
            dbo.udfConvertToLabel(vTableFieldsFromSys.FieldName) AS FieldLabel
    FROM    _TableFieldList AS _TableFieldList_1 RIGHT OUTER JOIN
            vTableFieldsFromSys ON _TableFieldList_1.TableName = vTableFieldsFromSys.TableName AND
            _TableFieldList_1.FieldName = vTableFieldsFromSys.FieldName
    WHERE   (_TableFieldList_1.TableFieldID IS NULL) AND (NOT (vTableFieldsFromSys.TableName LIKE N'sys%'))

    --Delete
    DELETE FROM _TableFieldList
    FROM    _TableFieldList LEFT OUTER JOIN
            vTableFieldsFromSys ON _TableFieldList.TableName = vTableFieldsFromSys.TableName AND
            _TableFieldList.FieldName = vTableFieldsFromSys.FieldName
    WHERE   (vTableFieldsFromSys.TableName IS NULL)

    Script for function udfConvertToLabel

    CREATE FUNCTION dbo.udfConvertToLabel
        (@FieldName varchar(255))
    --Converts a Field name to a label
    --parameter - string that needs to be spaced on _ or capital letter
    --returns - string - spaces inserted as needed.
    RETURNS varchar(255)
    AS
    BEGIN
        declare @pos int
        declare @label varchar(255)
        set @pos=0
        --certain fields to ignore
        if @FieldName in ('rowguid','LastUpdate','DeletedFlag') or @Fieldname Like 'FP%'
            return ''
        --get rid of field type systax
        IF @FieldName like '%ID'
            set @FieldName=substring(@FieldName,1,Len(@FieldName)-2)

        IF @FieldName like '%Flag'
            set @FieldName=substring(@FieldName,1,Len(@FieldName)-4)
            
        --spell out other abbreviations
        IF @FieldName like '%Desc'
            set @FieldName=@FieldName + 'ription'

        --look for underscore in string -replace with space
        set @pos= charindex('_',@FieldName,1)
        While @pos>0 begin
            set @FieldName=substring(@FieldName,1,@pos-1) + ' ' + substring(@FieldName,@pos+1,9999)
            set @pos=charindex('_',@FieldName,@pos+1)
        End    
        
        --insert space before single capital letter
        set @pos=1
        set @Label=''
        While @Pos<=len(@FieldName) begin
            --is it a capital letter
            if substring(@FieldName COLLATE Latin1_General_BIN,@pos,1) Like '[A-Z]'
                --is the previous char a lower case letter
                if substring(@FieldName COLLATE Latin1_General_BIN,@pos-1,1) Like '[a-z,0-9]'
                    set @Label=@Label + ' '
            set @Label=@Label + substring(@FieldName,@pos,1)
            set @pos=@pos+1
        end

        RETURN @label --@Fieldname
    END

    About the Author

    David Catherman - CMI Solutions

    Email: DCatherman (at) CMiSolutions (dot) com

    David Catherman has 20+ years designing and developing database applications with specific concentration for the last 4-5 years on Microsoft .NET and SQL Server. He is currently Application Architect and Senior Developer at CMI Solutions using Visual Studio 2005 and SQL Server 2005. He has several MCPs in .NET and is pursuing MCSD.

  • Rate This Article
    Not HelpfulMost Helpful
    1 2 3 4 5
    Other Articles
    Jul 21, 2005 - N-Tier Web Applications using ASP.NET 2.0 and SQL Server 2005 - Part 1
    While the .NET Framework made building ASP.NET applications easier then it had ever been in the past, .NET 2.0 builds on that foundation in order to take things to the next level. This article shows you to how to construct an N-Tier ASP.NET 2.0 Web application by leveraging the new features of ASP.NET 2.0 and SQL Server 2005.
    [Read This Article]  [Top]
    Apr 28, 2005 - New Files and Folders in ASP.NET 2.0
    With the release of ASP.NET 2.0, Microsoft has greatly increased the power of ASP.NET by introducing a suite of new features and functionalities. As part of this release, ASP.NET 2.0 also comes with a host of new special files and folders that are meant to be used to implement a specific functionality. This article examines these new files and folders in detail and provides examples that demonstrate how to utilize them to create ASP.NET 2.0 applications.
    [Read This Article]  [Top]
    Mar 10, 2005 - The DataSet Grows Up in ADO.NET 2.0 - Part 2, Cont'd
    Alex Homer continues his detailed look at the major changes to the DataSet class. In this part, he looks at two features that allow developers to work with data in a more structured and efficient way when using the DataSet with a SQL Server 2005 database server.
    [Read This Article]  [Top]
    Mar 9, 2005 - The DataSet Grows Up in ADO.NET 2.0 - Part 2
    Alex Homer continues his detailed look at the major changes to the DataSet class. In this part, he looks at two features that allow developers to work with data in a more structured and efficient way when using the DataSet with a SQL Server 2005 database server.
    [Read This Article]  [Top]
    Mar 3, 2005 - The DataSet Grows Up in ADO.NET 2.0 - Part 1, Cont'd
    In this article, Alex Homer looks at the changes between the version 1.x and version 2.0 DataSet and their associated classes, showing you how you can take advantage of the new features to improve your applications' capabilities and performance.
    [Read This Article]  [Top]
    Mar 2, 2005 - The DataSet Grows Up in ADO.NET 2.0 - Part 1
    In this article, Alex Homer looks at the changes between the version 1.x and version 2.0 DataSet and their associated classes, showing you how you can take advantage of the new features to improve your applications' capabilities and performance.
    [Read This Article]  [Top]
    Feb 16, 2005 - Writing a Custom Membership Provider for the Login Control in ASP.NET 2.0
    In ASP.NET 2.0 and Visual Studio 2005, you can quickly program custom authentication pages with the provided Membership Login controls. In this article, Dina Fleet Berry examines the steps involved in using the Login control with a custom SQL Server membership database.
    [Read This Article]  [Top]
    Dec 29, 2004 - ClickOnce Deployment in .NET Framework 2.0
    In this article, Thiru Thangarathinam examines .NET 2.0's new ClickOnce deployment technology that is designed to ease deployment of Windows forms applications. This new technology not only provides an easy application installation mechanism, it also eases deployment of upgrades to existing applications.
    [Read This Article]  [Top]
    Dec 15, 2004 - A Sneak Peek at ASP.NET 2.0's Administrative Tools
    With ASP.NET 2.0, Microsoft has made great strides in increasing developer productivity and has made implementing previously complex solutions relatively easy. Where this version of ASP.NET really shines, however, is in its new administrative tools that allow developers to spend less time managing the configuration of the servers and software and more time developing great code.
    [Read This Article]  [Top]
    Nov 17, 2004 - The ASP.NET 2.0 TreeView Control
    Thiru Thangarathinam introduces ASP.NET 2.0's new TreeView control which provides a seamless way to consume and display information from hierarchical data sources. The article discusses this new control in depth and explains how to use this feature rich control in your ASP.NET applications.
    [Read This Article]  [Top]
    Mailing List
    Want to receive email when the next article is published? Just Click Here to sign up.

    Support the Active Server Industry

    internet.commediabistro.comJusttechjobs.comGraphics.com

    Search:

    WebMediaBrands Corporate Info

    Legal Notices, Licensing, Reprints, Permissions, Privacy Policy.
    Advertise | Newsletters | Shopping | E-mail Offers | Freelance Jobs