.Net Grid Tutorial Part13: Editors
A grid without editors can not be called a grid. Convenient data editing feature is one of the most important advantages of Dapfor’s product over other grids.
In developing the editing system programmers considered that the grid should support standard .Net framework editors, enable easy use of new editors and controls and use any third-party editors and graphical controls.
Standard Microsoft DataGridView control for data editing supports different column types: DataGridViewTextBoxColumn, DataGridViewComboBoxColumn, etc. Many third-party grid developers use similar approach. However, we think that editor type shouldn’t depend on column type as this approach is too bulky and limits available editors, while the programmer often has to write a lot of code to implement a new editor. Besides, we think that a header column shouldn’t contain information of data types. Below we shall provide some examples of this.
NetGrid editing system is based on well-known proven data editing principle used in PropertyGrid. This approach is based on UITypeEditor class and supports use of almost any controls in a dropdown combo box.
Let’s say a few words about UITypeEditor. Many .Net classes have their own editors. They can be declared very easily:
class MyEditor : UITypeEditor
{
...
}
[Editor(typeof(MyEditor), typeof(UITypeEditor))]
class MyClass
{
...
}
To get an editor object the following code can be used.
UITypeEditor myEditor = (UITypeEditor)TypeDescriptor.GetEditor(typeof(MyClass), typeof(UITypeEditor));
UITypeEditor colorEditor = (UITypeEditor)TypeDescriptor.GetEditor(typeof(Color), typeof(UITypeEditor));
By the way, we see editors of this kind when we edit properties in VisualStudio. This is how Control.Dock property editing
looks in IDE.
UITypeEditor has several styles determined by UITypeEditorEditStyle enum. Data editing in different styles looks as follows.
Data editing starts on left-click over PropertyGrid cell. The grid reads current value of data object property and calls UITypeEditor.EditValue() method. Further implementation of editor completely depends on developer. With modal call the programmer just has to call Form.ShowModal() from of any user form. As the call is modal, the developer has to return a new value after closing the form. This is how font picker editor works.
class MyEditor : UITypeEditor
{
public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context)
{
return UITypeEditorEditStyle.Modal;
}
public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value)
{
Font font = value as Font;
FontDialog dlg = new FontDialog();
dlg.Font = font;
if(dlg.ShowDialog() == DialogResult.OK)
{
value = dlg.Font;
}
return value;
}
}
Usage of user controls in a dropdown combo box is not much more complicated. Implementation of IWindowsFormsEditorService interface ensures modal display of user control in a dropdown window. IWindowsFormsEditorService.DropDownControl() method is equivalent to Form.ShowModal() and to close control the programmer only has to call IWindowsFormsEditorService.CloseDropDown() method.
class MyEditor : UITypeEditor
{
public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context)
{
return UITypeEditorEditStyle.DropDown;
}
public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value)
{
IWindowsFormsEditorService service = provider.GetService(typeof(IWindowsFormsEditorService)) as IWindowsFormsEditorService;
if(service != null)
{
using(Button someControl = new Button())
{
someControl.Click += delegate
{
service.CloseDropDown();
};
service.DropDownControl(someControl);
}
}
return value;
}
}
.Net Grid uses similar approach and implements IWindowsFormsEditorService interface in the same way. This enables it to use standard controls and third-party controls for data editing.
Data editing in cells
As it has been previously said, UITypeEditor may have only 3 styles: (None, Modal, DropDown). If None style is applied, PropertyGrid uses TextBox. We think that it is a significant limitation as the programmer may desire to place his own control inside a cell and not in a dropbox (e.g. a slider). Dapfor NetGrid expands its features with UITypeEditorEx class. This class has an enhanced set of methods that can be used to create an arbitrary control directly over data cell and to use it for editing.
class MyEditor : UITypeEditorEx
{
public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context)
{
return UITypeEditorEditStyle.None;
}
public override StopEditReason EditCell(IGridEditorService service, Cell cell, StartEditReason reason)
{
using(Button button = new Button())
{
button.Click += delegate
{
service.CloseCellControl(StopEditReason.UserStop);
cell.Value = _newvalue_;
};
return service.CellEditControl(button, cell.VirtualBounds, reason);
}
}
}
Let’s note that user control should be created only for the time of data editing. It is especially important when the grid operates a lot of rows. Controls cannot be created for each row as Windows limits maximum number of controls created by the application. For this purpose UITypeEditorEx provides a feature of drawing control directly inside a cell. This way user feels comfortable when editing data as if he was working with actual controls. However, in reality the grid creates controls only when the user clicks a cell.
public class TrackBarEditor : UITypeEditorEx
{
private readonly int _minValue;
private readonly int _maxValue;
public TrackBarEditor(int minValue, int maxValue)
{
_minValue = minValue;
_maxValue = maxValue;
}
public override bool GetPaintCellSupported()
{
return true;
}
public override void PaintCell(PaintCellEventArgs e)
{
e.Text = string.Empty;
e.PaintAll();
e.Handled = true;
Rectangle r = e.Cell.VisibleBounds;
r.Inflate(-4, 0);
int value = (int) e.Cell.Value;
int range = _maxValue - _minValue;
if(range > 0)
{
Rectangle channel = new Rectangle(r.X, r.Y + r.Height / 2 - 3, r.Width, 4);
int offset = (value*(r.Width - 4)/range);
int x = r.X + offset;
if(e.Grid._impl.IsRightToLeft)
{
x = r.Right - offset - 4;
}
Rectangle button = new Rectangle(x, r.Y + 3, 5, r.Height - 6);
ControlPaint.DrawBorder3D(e.Graphics, channel, Border3DStyle.SunkenInner);
if (Application.RenderWithVisualStyles)
{
VisualStyleRenderer renderer = new VisualStyleRenderer(VisualStyleElement.TrackBar.ThumbBottom.Hot);
renderer.DrawBackground(e.Graphics, button);
}
else
{
ControlPaint.DrawButton(e.Graphics, button, ButtonState.Normal);
}
}
}
public override StopEditReason EditCell(IGridEditorService service, Cell cell, StartEditReason reason)
{
StopEditReason stopReason = StopEditReason.Undefined;
if (cell.Row != null && Equals(StartEditReason.LButtonClick, reason))
{
cell.EnsureVisible();
using (TrackBar control = new TrackBar())
{
control.RightToLeft = cell.Row.Grid._impl.IsRightToLeft ? RightToLeft.Yes : RightToLeft.No;
control.TabStop = true;
control.BackColor = cell.Appearance.BackColor;
control.TickStyle = TickStyle.None;
control.Minimum = _minValue;
control.Maximum = _maxValue;
int value = Convert.ToInt32(cell.Value);
value = Math.Max(control.Minimum, value);
value = Math.Min(control.Maximum, value);
control.Value = value;
control.MouseUp += delegate
{
service.CloseCellControl(StopEditReason.UserStop);
};
stopReason = service.CellEditControl(control, cell.VirtualBounds, reason);
cell.Value = Convert.ChangeType(control.Value, cell.DataField.FieldType);
}
}
return stopReason;
}
}
Specific case – data editing without graphical controls.
In some cases it is not necessary to create controls. This may happen when a data field contains bool value or a star rating assigned by user. Such values don’t require control creation but only painting current state and reacting on user click on a cell.
public class RatingEditor : UITypeEditorEx
{
public override bool GetPaintCellSupported()
{
return true;
}
public override void PaintCell(PaintCellEventArgs e)
{
e.Text = string.Empty;
e.PaintAll();
e.Handled = true;
Rectangle bounds = e.Cell.VirtualBounds;
bounds.Width = Resources.star_grey.Width;
for (int i = 0; i < 5; i++)
{
if (e.Cell.Value != null && e.Cell.Value is int)
{
int curRating = (int) e.Cell.Value;
Image image = i >= curRating ? Resources.star_grey : Resources.star_yellow;
e.Graphics.DrawImage(image, bounds);
bounds.X += bounds.Width;
}
}
}
public override StopEditReason EditCell(IGridEditorService service, Cell cell, StartEditReason reason)
{
if (cell.Row != null && Equals(StartEditReason.LButtonClick, reason))
{
Point pt = cell.Row.Grid.PointToClient(Cursor.Position);
Rectangle bounds = cell.VirtualBounds;
int imageWidth = Resources.star_grey.Width;
if (bounds.Contains(pt) && imageWidth > 0)
{
cell.Value = ((pt.X - bounds.X)/imageWidth) + 1;
cell.Invalidate();
}
}
return StopEditReason.UserStop;
}
}
Enabling editing
The following conditions should be met for data editing by user:
Editor setup
There are several methods that can be used for editor setup.
As it has been previously said, editors can be set only for object types
[Editor(typeof(MyEditor), typeof(UITypeEditor))]
class MyClass
{
...
}
Dapfor has expanded use of these attributes for class properties. For example, the following code can be used for data editing with a rating editor:
class SomeProduct
{
private int _rating;
[Editor(typeof(RatingEditor), typeof(UITypeEditor))]
public int Rating
{
get { return _rating; }
set { _rating = value; }
}
}
If grid rows contain objects of different types, the grid may use different editors depending on class properties.
- Editors can be set in columns:
Header header = grid.Headers[0];
header["rating"].Editor = new RatingEditor();
- Editors can also be set directly when the user clicks a cell. This feature can be used to disable editing of some cells:
grid.CellBeginEdit += delegate(object sender, GridCellBeginEditEventArgs e)
{
if (e.Cell.Column != null && e.Cell.Column.Id == "rating")
{
e.Editor = new RatingEditor();
}
};
Validation
Users may enter wrong data when editing. A programmer can use validation features to transmit only correct values to data objects. With editors based on UITypeEditorEx this is quite simple as the programmer has full control over data editing and therefore controls the validation process and transmission of new value to a data object.
public override StopEditReason EditCell(IGridEditorService service, Cell cell, StartEditReason reason)
{
using(SomeControl control = new SomeControl())
{
button.Click += delegate
{
if(control.IsValid)
{
service.CloseCellControl(StopEditReason.UserStop);
}
};
return service.CellEditControl(control, cell.VirtualBounds, reason);
}
}
A similar approach can be used for editors based on UITypeEditor. Besides, the grid provides an interface that controls grid behavior in data editing and validation. The following example shows how to display a tooltip error message upon incorrect data entry.
grid.ValidateCell += delegate(object sender, ValidateCellEventArgs e)
{
e.ErrorText = "Invalid data";
e.Action = ValidateCellAction.CancelValue;
};
Navigation
The grid provides editor navigation feature to make data editing easier for users. It is possible to set the reason for stopping cell editing, such as Esc, Enter, Tab keys, Shift+Tab shortcut, etc. Depending on the reason the grid decides whether editing should be continued or ended. Grid reactions on completion of data sorting in a cell are shown in the table below.
Reason | Reaction |
---|
StopEditReason.SelectValue | Navigation to the next editable cell |
StopEditReason.Enter | Navigation to the next editable cell |
StopEditReason.Tab | Navigation to the next editable cell |
StopEditReason.ShiftTab | Navigation to the previous editable cell |
StopEditReason.LButtonOutsideClick | Editing a new cell, over which the cursor is |
StopEditReason.Escape | End of editing the current cell |
StopEditReason.Exception | End of editing the current cell |
The grid provides the following 2 callbacks to determine next or previous cell for editing.
Therefore, the programmer may specify any desired cell for this purpose.
grid.PrevEditableCell += delegate(object sender, GridEditableCellEventArgs e)
{
e.NewEditableCell = _a_new_editable_cell;
};
grid.NextEditableCell += delegate(object sender, GridEditableCellEventArgs e)
{
e.NewEditableCell = _a_new_editable_cell;
};
We hope that reading this tutorial will make data editing in NetGrid easy and clear for you. We also hope that efforts
of Dapfor’s programmers will result in creation of beautiful and compact applications.