Dapfor .Net Grid Introduction

Part1: Data Types
Part2: Data Binding
Part3: Event-driven model
Part4: Data formatting and presentation
Part5: Threadsafety
Part6: Threadsafe BindingList
Part7: Appearance and painting
Part8: Cell highlighting
Part9: Data filtering
Part10: Data sorting
Part11: Data grouping
Part12: Headers and columns
Part13: Editors
Part14: Performance. Practical recommendations
Part15: Real-time blotter
Part16: Currency converter
Part17: Instrument browser
Part18: Order book
Part19: Basket Viewer



.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.

grid-intro 13

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.


//Custom editor
UITypeEditor myEditor = (UITypeEditor)TypeDescriptor.GetEditor(typeof(MyClass), typeof(UITypeEditor));

//Standard editor to edit Color objects
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.

Editors in PropertyGrid

UITypeEditor has several styles determined by UITypeEditorEditStyle enum. Data editing in different styles looks as follows.

Editor styles

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)
{
//Get the editor service
IWindowsFormsEditorService service = provider.GetService(typeof(IWindowsFormsEditorService)) as IWindowsFormsEditorService;

if(service != null)
{
//Create a control
using(Button someControl = new Button())
{
//Add a reaction on user click
someControl.Click += delegate
{
//User has clicked on the button inside of the dropdown box. Close the control.
service.CloseDropDown();

//Set a desired value
//value = _new_value_;
};

//Start editing with specified control.
//This call opens a modal dropdown box
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.


//This editor creates a clickable button inside the cell
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())
{
//Add a reaction on user click
button.Click += delegate
{
//Close control with some reason
service.CloseCellControl(StopEditReason.UserStop);

//Set a new value if needed
cell.Value = _newvalue_;
};

//Start editing in cell
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;

//Constructor
public TrackBarEditor(int minValue, int maxValue)
{
_minValue = minValue;
_maxValue = maxValue;
}

//Indicates that the editor draws the entire cell
public override bool GetPaintCellSupported()
{
return true;
}

//Painting routine
public override void PaintCell(PaintCellEventArgs e)
{
//Do basic painting without text
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);

//Draw the track channel
ControlPaint.DrawBorder3D(e.Graphics, channel, Border3DStyle.SunkenInner);

//Draw the thumb
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;
//Edit the cell if the user has clicked on the cell with the left button
if (cell.Row != null && Equals(StartEditReason.LButtonClick, reason))
{
cell.EnsureVisible();
//Create a real control
using (TrackBar control = new TrackBar())
{
control.RightToLeft = cell.Row.Grid._impl.IsRightToLeft ? RightToLeft.Yes : RightToLeft.No;

//Initialize the control
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;

//Handle the MouseUp events
control.MouseUp += delegate
{
//Stop edit in place
service.CloseCellControl(StopEditReason.UserStop);
};

//Start Edit in place.
stopReason = service.CellEditControl(control, cell.VirtualBounds, reason);

//Set a new value to Cell
cell.Value = Convert.ChangeType(control.Value, cell.DataField.FieldType);
}
}
return stopReason;
}
}
Trackbar editor

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()
{
//The cell should be repainted in the editor
return true;
}

public override void PaintCell(PaintCellEventArgs e)
{
//Do basic painting without text
e.Text = string.Empty;
e.PaintAll();
e.Handled = true;

//Draw the stars over the cell
Rectangle bounds = e.Cell.VirtualBounds;
bounds.Width = Resources.star_grey.Width;
for (int i = 0; i < 5; i++)
{
//Select image
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)
{
//Edit the cell if the user has clicked on the cell with the left button
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)
{
//Set a new rating to the data object
cell.Value = ((pt.X - bounds.X)/imageWidth) + 1;
cell.Invalidate();
}
}

return StopEditReason.UserStop;
}
}
Rating editor

Enabling editing

The following conditions should be met for data editing by user:

  • Property of the edited object should have a setter.

  • Column.Editable property should be set to true

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
{
//validate user's input. If data is correct, close the editor
if(control.IsValid)
{
service.CloseCellControl(StopEditReason.UserStop);
}
};

//Start editing in cell
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;
};
Data validation

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.