
The problem
How many times where you required to let users select some data from a database? This means allowing the user to check some ticks on the data from the database and then to perform some actions based on the selected items.
In ASP.NET 2.0, there are two controls that could do the trick: CheckBoxList
and RadioButtonList
. These controls allow you to set a DataSource
property to them, and they will handle all the work. However, if you need to get the selected items in the list, you have to do that by hand, checking each element if it is selected. This is not a bad approach, it is just error prone one, because you have to write the same code over and over again.
Another issue is the fact that I do not have a way of choosing how the elements will be displayed. If I want, at some point, to change the way elements get selected from a multiple selection to a single selection, then I need to remove the CheckBoxList
, add the RadioButtonList
, and then (re)write the necessary logic to make it work. This is a big productivity issue because instead of being able to change a single property, you need to remake it from scratch (almost).
What if you need to preselect some of the elements in the list you are displaying? The regular way would be to investigate each element as it comes from the database and set the Selected
property on the bound item. Wouldn't it be nice if you could just pass an array of elements to be selected to a control and have it do all the work?
These are the problems I am addressing with this article.
Goals:
- I want to have a control that will permit me to display selections to users.
- I want to be able to change the display method from single select (i.e.,
RadioButton
) to multiple select (i.e., CheckBox
) by changing a single property. - I want to be able to retrieve a list of selected items from the control.
- I want to be able to preselect some items in the list by passing in an array of selected elements.
A little background
In a selection dialog, we usually want to be able to display some text to the user, and when the user selects that text, to have a way of associating the text with a more meaningful element from the data, say the primary key of the selected element.
So, besides showing the text to the user, we must also keep the data that the element represents. This is what happens in ASP.NET also. For a ComboBox
, you have a SelectedText
and a SelectedValue
. SelectedText
is the text displayed to the user, and SelectedValue
is the meaningful value (database wise) of that text.
Let's say we have some data that we need to display to the user and allow them to select some items from it. You could pass in a DataTable
and specify which column is the display column and which columns is the value, or you could just pass in a Connection String and a SQL query instead of a DataTable
and have the control retrieve the data from the database. This control will support both scenarios.
We need this dual approach because setting the Connection String and SqlCommand
can be done in Design, while setting a DataTable
is most likely to happen in code. Because a connection string can be stored in a web.config, you could just pass the name of the configuration element where the connection string is stored.
The idea
The idea is to use existing controls from ASP.NET and add some extra functionality to them. For example, we could use the CheckBox
control to display an item in a multi-select list, and a RadioButton
to display an element when in simple select mode. This way, we reuse the existing code and its functionality.
For instance, a RadioButton
will deselect all of the other RadioButton
s in a group when it is selected. This is a very useful behavior that we get "for free".
Solution
When stating this problem, the solution becomes clear very quickly. We get a DataTable
. For each row in that DataTable
, we must create a corresponding control. The type of control will be determined based on the selection mode specified. If we want single selection, we will add RadioButton
s, and if we want multiple selection, we would add CheckBox
es.
There is only one catch here: we need to store two pieces of information in a CheckBox
(or RadioButton
) control:
- Text - which represents the part that is displayed to the user.
- Value - the corresponding value associated with the text. This value should not be displayed to the user.
The problem is that, by design, a CheckBox
or a RadioButton
only has room to store one of these, in a property called Text
. Of course, one could choose to embed the associated value with the ID of the control (the ID is used to uniquely identify the control on a page).
I have seen this practice and I am not comfortable with it. The reason is that, in order to retrieve the value from the control, we must resort to some string manipulation. Moreover, the value is available in clear text in the page source, and thus it is vulnerable to change. Imagine someone using as a value the Credit Card number of a user. That data would be available in clear text to anybody!!!
So we need to find a way of not showing the Value
in clear text.
Fortunately, this is not such a big problem. Because a CheckBox
is a class in .NET, we can simply derive from that class and create our own CheckBox
and call it CheckBoxWithValue
. In this derived class, we can declare a property Value
that can contain the associated value for a control. Because we are deriving from the CheckBox
class, we get the behavior of the CheckBox
with no extra charge. We can now use our derived control as if it was a regular CheckBox
. And, it will feel like a CheckBox
. The only difference will be in the fact that now, we have a way of storing both the Text
and the Value
in one control.
internal class CheckBoxWithValue : System.Web.UI.WebControls.CheckBox
{
private object myValue;
public object Value
{
get { return myValue; }
set { myValue = value; }
}
}
The Value
is implemented as an object
in order to have the possibility to store any type of object in that property.
What do we gain by doing this? Well, besides the fact that now there is room to store the associated value in the control, the value is also encrypted. The encryption is part of the ASP.NET ViewState mechanism. The ViewState will encrypt all data regarding the controls on the form, and this includes our controls, thus the data will not be available to an attacker. Furthermore, ASP.NET will detect any change to the ViewState and report it to the user. More information regarding the ViewState can be found here.
Now that we have this class, we can use it in order to create the list of controls. The code that builds the list of controls is shown below:
foreach (DataRow myRow in myData.Rows)
{
Control myControl = null;
switch (myOptionsType)
{
case OptionsSelect.Single:
{
RadioButtonWithValue myBut = new RadioButtonWithValue();
myBut.Text = myRow[myDisplayField].ToString();
myBut.Value = myRow[myValueField];
myBut.ID = string.Format("{0}_{1}", "RadioButton", i++);
myBut.GroupName = string.Format("GroupName{0}", this.ID);
if (i == 1)
myBut.Checked = true;
myControl = myBut;
break;
}
case OptionsSelect.Multiple:
{
CheckBoxWithValue myCheck = new CheckBoxWithValue();
myCheck.Text = myRow[myDisplayField].ToString();
myCheck.ID = string.Format("{0}_{1}", "CheckBox", i++);
myCheck.Value = myRow[myValueField];
myControl = myCheck;
break;
}
}
ObjectsPanel.Controls.Add(myControl);
ObjectsPanel.Controls.Add(new LiteralControl("<br/>"));
}
You can see that we add the RadioButton
s to a single group of elements. We do this so we can benefit from the auto-deselect of other items when we select a new one.
The control also provides the means of retrieving the selected elements in the list. For this, we look at each control that was added and check its Checked
property. If the property is set, then we add the Value
of it to a list. After we look at all of the controls, we return the list.
List<object> myItems = new List<object>();
switch (myOptionsType)
{
case OptionsSelect.Single:
{
foreach (Control myControl in ObjectsPanel.Controls)
{
RadioButtonWithValue myRadio = null;
if (myControl.GetType() == typeof(RadioButtonWithValue))
{
myRadio = (RadioButtonWithValue)myControl;
if (myRadio.Checked == true)
{
myItems.Add(myRadio.Value);
break;
}
}
}
break;
}
case OptionsSelect.Multiple:
{
foreach (Control myControl in ObjectsPanel.Controls)
{
CheckBoxWithValue myCheck = null;
if (myControl.GetType() == typeof(CheckBoxWithValue))
{
myCheck = (CheckBoxWithValue)myControl;
if (myCheck.Checked == true)
{
myItems.Add(myCheck.Value);
}
}
}
break;
}
}
return myItems.ToArray();
This function is actually a get
accesor for the property SelectedItems
of the control. This property adheres to the design pattern of other controls that expose this functionality (i.e., WinForms ListBox
).
We can also provide the control with an InitialState
, by passing an array of objects representing the Value
s of the elements that need to be selected.
List<object> myItems = new List<object>();
foreach (Control myControl in ObjectsPanel.Controls)
{
if (myControl.GetType() == typeof(RadioButtonWithValue))
{
RadioButtonWithValue myButt = (RadioButtonWithValue)myControl;
foreach (object o in values)
{
if (myButt.Value.ToString() == o.ToString())
myButt.Checked = true;
}
}
else if (myControl.GetType() == typeof(CheckBoxWithValue))
{
CheckBoxWithValue myButt = (CheckBoxWithValue)myControl;
foreach (object o in values)
{
if (myButt.Value.ToString() == o.ToString())
myButt.Checked = true;
}
}
}
And with this function, the functionality of the control is complete. You can find the source code for the control at the top of this article, along with a sample web site that uses it. Read more to see how you can use this control.
Using the control
Because the OptionsControl
is implemented as a UserControl
, it can be dragged & dropped from Solution Explorer onto a page.
There are a few properties that need to be set on the control before it can be used. Below, you will find a table of the properties, its role, and a suggested value.
Property | Description |
Height | The height of the control. Can be left blank. |
Width | The width of the control. Can be left blank. |
OptionsType | The select mode. Available options are:
|
ConnectionString | The connection string used to connect to the database. This value can contain either a full connection string or the name of the connection string from the web.config where the full connection string is stored.
This allows you to use the same connection string in more places.
|
CommandString | The SQL query that should be run on the database in order to retrieve the data. This value can be left blank if the data is set on the control by using the DataTable property. |
CommandType | The type of the command to use. This has to be set to StoredProcedure when the text specified in CommandString is the name of a Stored Procedure. When we specify a SQL query, this property must be set to Text . |
DisplayField | The name of the column in the database that contains the text to be displayed by the user. |
ValueField | The name of the column in the database that contains the value that will be associated with the text. |
DataTable | The DataTable to use for displaying controls instead of retrieving data from the database. |
InitialState | An array of objects that contains the values that must be checked. This array should contain the elements specified in the ValueField , and not the ones in DisplayField . |
For a demonstration of this control, please refer to the top of the article where you will find both the control and a web site that utilizes the control.
Conclusion
In this article, I have described the way to build a user control that allows single and multiple selection of elements from a DataTable
.
The control created follows the goals set in the introduction, and can be used in ASP.NET web applications in order to display a selection of items to users and allow them to choose from them.
Hopefully you enjoyed the read and you will use this control in your applications.
Happy coding!