Introduction
The application I'm currently working on relies heavily on the use of the .NET PropertyGrid
to allow the user to view and modify data.
The PropertyGrid
in its original format allows us to do most of the things we need. With little effort, it allows us to use data conversion, localization, grouping, and other forms of data presentation. Input formatting is one of the major drawbacks we have to overcome when using the PropertyGrid
. This capability is almost totally absent from this control. Yet, we have a need to sometimes present data in a different format then it is stored.
The attached example shows how the PropertyGrid
can be used to display password text fields and also formatted numeric string fields. In the example, the formatted properties are initially set to be blank values. To see the data formatting, you can simply start typing in the input field of the PropertyGrid
.
Background
When presenting the data to the user, we ran into requirements to have a "password" field and formatted data input fields. With the password fields, the requirements are obvious. When the user types in this kind of a field, the typed characters are masked; typically, they are masked using the asterisk ("*") character. For the "formatted" input, the requirement is that the user must be able to input data using a predefined format only. This is mostly used when inputting numeric values with the decimal point separator embedded in them. For an additional twist, the decimal point indicator should be culture specific; so for US it is a period ("."), for most European customers, it would be a comma (",").
For example:
The format may look like this: "---.-- ". In this case, the user would be able to enter only numbers with three significant digits and, at the most, two decimal places. For the specific application we are working on, this type of formatting is very important. If the field does not contain any data, the format mask is displayed to inform the user how the data should be inputted. When the data is typed in the field, the entered text overwrites the format.
The .NET PropertyGrid
with all its powers did not provide a straightforward solution to the above problems. I did some research on the Web to figure out how to implement password fields within the PropertyGrid
control, but what I found were mostly questions and a few answers. So, I decided to do the work myself.
My first approach was to embed the editing control into the password property similar to the way date/time values are edited. In this case, the user clicks on the down arrow located at the right side of the edit field. This brings up the editor window into view; the user enters the data into the window and then presses the additional buttons to close the editor. This is a very simple and cheap approach. The users felt that this was too cumbersome and hard to use, so they requested that the password must be editable within the control's edit fields without using extra edit windows or controls.
A similar action was requested for the "formatted" field issue so they would work the same way.
To solve this problem, I decided that the easiest solution would be to create a custom grid control which derives from the .NET native PropertyGrid
and then write all the necessary code to achieve this functionality for both of these requirements.
Using the code
First, derive the custom control class from the existing .NET PropertyGrid
control. This is a trivial task, and in my case, the code looked like this, where CustomPropertyGrid
is the name of my derived custom control. The private variables defined are used for different purposes which will be apparent from further code study. From that point, all that had to be done was to write code inside this class to create the desired functionality.
public class CustomPropertyGrid: System.Windows.Forms.PropertyGrid
{
private System.Windows.Forms.Control.ControlCollection gridControls = null;
private TextBox m_TextBox = null;
private int lastSelection;
private string formatingString = "";
private char formatSeparator = '.';
#region PROPERTIES
#endregion PROPERTIES
#region METHODS
#region CustomPropertyGrid Method
public CustomPropertyGrid() : base()
{ }
#endregion CustomPropertyGrid Method
}
The initial challenge was to find out what method to overwrite in the derived class to accomplish the desired effects. As it turned out, I only had to overwrite two methods: OnSelectedGridItemChanged(SelectedGridItemChangedEventArgs e)
and OnSelectedObjectsChanged(EventArgs e).
The first method sets the control in the mode allowing the password character masks, while the second one is used to reset the control to the default state and gather the necessary formatting data from the object being displayed within the grid. Of course, there are additional peripheral methods which aid the PropertyGrid
's overwritten methods; and all these are described in this article.
As the user traverses the edit fields within the grid, there is a potential that they will end up on the password or formatted data field. Also, every time this happens, the OnSelectedGridItemChanged
method is called. Because of this, we can gather all the information we need to perform data formatting when the method is called.
The first thing we need to do is to get the active control and selected item. This is done in the first few lines of the code in the method below. Once we have that, we called our own method SetupPropertyStyle
. This method makes sure that we are on the password field. In such a case, it masks the field's input data; otherwise, it resets the mask to null
. The method's logic is described later in this document.
In the second part of the OnSelectedGridItemChanged
overwritten method, we grab a hold of an actual edit control (TextBox
) for the property and attach some event handlers to it. Please note the interesting method used to determine if the control is actually the one we are interested in. Since the type reported by the PropertyGrid
for this control is an internal
(.NET) type, we have to do a hard coded string comparison. Of course, this approach is very static and will stop working if .NET changes the name of this type under us, but such is life. If that happens, we would have to modify our code and change the type name we are looking for to the new one.
So first of all, the SetupPropertyStyle
method called at the beginning will mark the field editing properties to allow for the input masking. Also, in the same overwritten method, we are extracting the TextBox
control so later on we are able to modify how its input is treated.
Once we have the TextBox
control and we determine that the property requires special treatment of its input, we subscribe to the TextChanged
event of the TextBox
control. In other cases, the extracted control will not be used. Subscribing to the event allows us to respond to each key stroke within the TextBox
control and to modify its contents at will. The actual implementation of the event handler is shown below. In order to determine if the currently edited property needs special treatment (is it a password field or maybe a formatted data field), I have introduced a custom attribute which I can use to decorate certain properties of my display object. The OnSelectedGridItemChanged
method logic below shows the reference to that attribute, which is called PropertyViewFormatAttribute
. This custom attribute will tell me if I'm dealing with a "password" type field or a "formatted" input type field. I have attached example code, which shows clearly how the property is implemented and used within the displayed object class.
protected override void
OnSelectedGridItemChanged(SelectedGridItemChangedEventArgs e)
{
Control activeControl;
GridItem selectedItem;
activeControl = base.ActiveControl;
selectedItem = base.SelectedGridItem;
if( activeControl != null && selectedItem != null )
{
this.SetupPropertyStyle( activeControl, selectedItem );
}
if (m_TextBox != null )
{
m_TextBox.TextChanged -=
new EventHandler(m_TextBox_TextChanged);
}
if( m_TextBox == null )
{
foreach (Control control in base.ActiveControl.Controls)
{
if (control.GetType().ToString() ==
"System.Windows.Forms.PropertyGridInternal." +
"PropertyGridView+GridViewEdit" )
{
if (control is TextBox)
{
m_TextBox = control as TextBox;
break;
}
}
}
}
foreach( System.Attribute attribute in
selectedItem.PropertyDescriptor.Attributes )
{
if( attribute.GetType() == typeof(PropertyViewFormatAttribute) )
{
if( ( ((PropertyViewFormatAttribute)attribute).AttributeFlags &
PropertyViewAttributeEnum.PropertyViewAttributes.FormattedField)
== PropertyViewAttributeEnum.PropertyViewAttributes.FormattedField )
{
if( m_TextBox != null )
{
lastSelection = m_TextBox.SelectionStart;
m_TextBox.TextChanged +=
new EventHandler(m_TextBox_TextChanged);
}
break;
}
}
}
base.OnSelectedGridItemChanged (e);
}
The OnSelectedObjectsChanged(EventArgs e)
method is much simpler; its main task is to restore the PropertyGrid
style to the default values. It is called when a new object is assigned to the PropertyGrid
control. This method also inquires about any data formatting information the displayed object might want to use for the "formatted" fields. In order to do that, a simple interface is implemented (the implementation is also shown in the attached sample project ). If the object passed to the PropertyGrid
implements this interface, then the formatting data is obtained from the object for later use. The interface, as it is currently implemented, allows the PropertyGrid
to get the format mask string and separator character from the SelectedObject
. This information is kept inside the PropertyGrid
object and used when the "formatted" field data is being edited.
This method is implemented like this:
protected override void OnSelectedObjectsChanged(EventArgs e)
{
if( this.SelectedObject is IPropertyGrid )
{
this.formatingString =
((IPropertyGrid)this.SelectedObject).FormatString;
this.formatSeparator =
((IPropertyGrid)this.SelectedObject).FormatSeparator;
}
if( gridControls != null )
this.ResetPropertyStyle( gridControls );
base.OnSelectedObjectsChanged (e);
}
The first line of code gets the formatting string and the format separator (remember: "." for US, and "," for Europe) from the SelectedObject
. In order to do this, the displayed object must implement the IPropertyGrid
interface.
The ResetPropertyStyle
method basically clears the edit field setting applied by the SetupPropertyStyle
method so the new object can be displayed without the password character masks in place.
Now, what I need to show you is how the setting and resetting of the property values (performed in SetupPropertyStyle
and ResetPropertyStyle
methods respectively) works. The first method sets the editing field to be a password type field and it looks like this:
private void SetupPropertyStyle(Control thisControl, GridItem viewItem)
{
bool passwordField;
System.ComponentModel.PropertyDescriptor viewProperty;
System.ComponentModel.AttributeCollection attributes;
System.Windows.Forms.Control.ControlCollection controls =
thisControl.Controls;
gridControls = controls;
viewProperty = viewItem.PropertyDescriptor;
attributes = viewProperty.Attributes;
passwordField = false;
foreach( System.Attribute attribute in attributes )
{
if( attribute.GetType() == typeof(PropertyViewFormatAttribute) )
{
if( ( ((PropertyViewFormatAttribute)attribute).AttributeFlags &
PropertyViewAttributeEnum.PropertyViewAttributes.PasswordField)
== PropertyViewAttributeEnum.PropertyViewAttributes.PasswordField )
{
passwordField = true;
break;
}
}
}
if( passwordField )
{
foreach( Control control in controls )
{
System.Type baseType = control.GetType().BaseType;
System.Reflection.PropertyInfo passwordCharProperty =
baseType.GetProperty( "PasswordChar" );
if( passwordCharProperty != null )
passwordCharProperty.SetValue( control, '*', null );
}
}
else
{
foreach( Control control in controls )
{
System.Type baseType = control.GetType().BaseType;
System.Reflection.PropertyInfo passwordCharProperty =
baseType.GetProperty( "PasswordChar" );
if( passwordCharProperty != null )
passwordCharProperty.SetValue( control, null, null );
}
}
}
In the SetupPropertyStyle
method, all the necessary data elements are obtained up front. These are the property attributes, the property descriptor, and all the controls. Again, using the PropertyViewFormatAttribute
, we determine if the current property needs any special formatting. These attributes will be attached in our data object class to the properties which are either password fields or require special formatting. In any case, we know that we will be dealing with the editing control which for the PropertyGrid
is TextBox
. Knowing this, and if the property is marked as a password field, then using .NET reflection capabilities, we get access to the PasswordChar
property of the TextBox
control we are dealing with. Then, we simply set this property to the password mask ("*") character. From now on, all the inputs into this control will use the PasswordChar
as a mask.
That's all that needs to be done to get the "password" fields to mask their input. If the property displayed does not need any formatting, then the password mask is reset so we can deal with "normal" fields within the same grid as well.
private void ResetPropertyStyle(
System.Windows.Forms.Control.ControlCollection thisControls)
{
foreach( Control control in thisControls )
{
System.Type baseType = control.GetType().BaseType;
System.Reflection.PropertyInfo passwordCharProperty =
baseType.GetProperty( "PasswordChar" );
if( passwordCharProperty != null )
passwordCharProperty.SetValue( control, null, null );
}
}
To reset the masking at the beginning of the display process, the ResetPropertyStyle
method is used. This method simply walks through all the controls passed to it and if the control has the PasswordChar
property (again obtained using the reflection methods), then the property value is set to null
, thus removing the mask. At this point, we don't care if the property has a Password
or Formatting
attribute set or not; we are just trying to establish the starting point (default) value here, in case the previously displayed object had formatted data fields.
Finally, I have a few words about the TextChanged
event handler that allows for the input of the formatted data. The event handler code looks like this:
private void m_TextBox_TextChanged(object sender, EventArgs e)
{
System.Text.StringBuilder newText;
string oldText = this.m_TextBox.Text;
int stringLen = this.m_TextBox.Text.Length;
int currentSelection = this.m_TextBox.SelectionStart;
System.Type senderType = sender.GetType();
System.Reflection.PropertyInfo property =
senderType.GetProperty( "Modified" );
bool mod = (bool)property.GetValue( sender, null );
if( !mod )
return;
newText = new System.Text.StringBuilder(formatingString.Length);
for( int i = 0, j = 0; i < formatingString.Length; i++ )
{
if( i < stringLen )
{
if( oldText[i] >= '0' && oldText[i] <= '9' )
{
if( formatingString[i] == formatSeparator )
{
newText.Insert( j, formatingString[i] );
j++;
newText.Insert( j, oldText[i] );
if( this.lastSelection < currentSelection )
currentSelection++;
else
currentSelection--;
}
else
{
newText.Insert( j, oldText[i] );
}
}
else
newText.Insert( j, formatingString[i] );
}
else
newText.Insert( j, formatingString[i] );
j++;
}
this.m_TextBox.Text = newText.ToString();
if( currentSelection < 0 )
currentSelection = 0;
this.m_TextBox.Select(currentSelection, 0);
this.lastSelection = currentSelection;
}
This method does the input formatting only for the properties which are marked to take the "formatted" input using our custom attributes. Again, .NET reflection is used to obtain all the necessary pieces of information. The biggest challenge when writing this method was to find out how to prevent data from being formatted when the TextBox
value was originally set. At this point, the event is triggered automatically and the event handler method is called. Even if the field didn't require special formatting, the displayed data is modified by the code shown above. The solution is to find the property of the sender object which indicates if the data was just set or actually modified by the user. Of course in our case, I'm only interested in the actual events generated by the user and not by the initial value setting. Using the debugger and studying the reflected properties of the sender object, I found that the Modified
boolean property on the sender contains the information I needed; it is set to true
when the value is modified by the user and reset to false
when initially set. The rest of the code in this method deals with data formatting using the format string and the separator character obtained in the OnSelectedObjectsChanged
method and it should be self explanatory.
Points of Interest
An additional benefit gained while implementing the formatted fields is that we were able to change the PropertyGrid
navigation mode to suit our customer's needs. For example the "Tab" key is used to navigate from one property edit field to another, which is not supported by the native .NET control. This was important for our customer and made the property grid easier for them to use. The implementation of this functionality, however, is not covered in this article. This was an additional challenge since the PropertyGrid
categories needed to be taken into account. Also, one needs to be aware of the state (expanded or collapsed) of the categories. If someone is interested in this topic, I can respond privately or cover this in an additional article at a later time.
Krzysztof "Kriss" Stoj works in the Software Development group for Itron Inc.
He is currently responsible for the UI development of the new meter reading system. He is also responsible for overlooking the localization aspects of the product.