Embedded Controls

Embedded editing controls can be thought of as an extension to label edit functionality (see Label Editing for more information).

Every item and sub-item has a cell area on which an editing control can be placed. In the simplest case (LabelEdit set to true), the editing control is basically a System.Windows.Forms.TextBox control. It is actually an instance of BetterListViewTextBoxEmbeddedControl, which is a TextBox wrapper implementing IBetterListViewEmbeddedControl interface. Any control can be used as embedded control in Bettter ListView if implements one of these interfaces:

The custom embedded control is shown on the image below. When user clicks on sub-item text (an abbreviation of tea grading), an editing control appears on the top-left corner of the cell area. The control contains buttons for accepting and cancelling changes:

Implementing IBetterListViewEmbeddedControl

This interface contains prescription for minimum amount of functionality required by an embedded control:

Let's make a sample control. We will make a TextBox-based embedded control for editing words in lower-case. First, we inherit TextBox and implement IBetterListViewEmbeddedControl interface:

C#

/// <summary>
///   Represents a custom control embeddable in Better ListView.
/// </summary>
public class TextBoxEmbeddedControl : TextBox, IBetterListViewEmbeddedControl

Visual Basic

''' <summary>
'''   Represents a custom control embeddable in Better ListView.
''' </summary>
Public Class TextBoxEmbeddedControl
    Inherits TextBox
    Implements IBetterListViewEmbeddedControl

Then we implement the LabelText property:

C#

/// <summary>
///   current (edited) label text
/// </summary>
public string LabelText
{
    get
    {
        return Text.ToLower();
    }
}

Visual Basic

''' <summary>
'''   current (edited) label text
''' </summary>
Public ReadOnly Property LabelText() As String
    Get
        Return Text.ToLower()
    End Get
End Property

As you can see, the text of the TextBox is converted to lower case since we want item/sub-item labels to be only in lower case.

Next, we implement RequestAccept and RequestCancel events:

C#

/// <summary>
///   request accepting updated data in BetterListView
/// </summary>
public event EventHandler RequestAccept;

/// <summary>
///   request cancelling editing
/// </summary>
public event EventHandler RequestCancel;

Visual Basic

''' <summary>
'''   request accepting updated data in BetterListView
''' </summary>
Public Event RequestAccept As EventHandler Implements IBetterListViewEmbeddedControl.RequestAccept

''' <summary>
'''   request cancelling editing
''' </summary>
Public Event RequestCancel As EventHandler Implements IBetterListViewEmbeddedControl.RequestCancel

Next, we implement GetData and SetData methods:

C#

/// <summary>
///   get data from the specified sub-item in control
/// </summary>
/// <param name = "subItem">sub-item whose data are being edited</param>
public void GetData(BetterListViewSubItem subItem)
{
	Text = subItem.Text;
}

/// <summary>
///   set data from control to the specified sub-item
/// </summary>
/// <param name = "subItem">sub-item whose data are being edited</param>
public void SetData(BetterListViewSubItem subItem)
{
    subItem.Text = LabelText;
}

Visual Basic

''' <summary>
'''   get data from the specified sub-item in control
''' </summary>
''' <param name = "subItem">sub-item whose data are being edited</param>
Public Sub GetData(ByVal subItem As BetterListViewSubItem) Implements IBetterListViewEmbeddedControl.GetData

    Text = subItem.Text

End Sub

''' <summary>
'''   set data from control to the specified sub-item
''' </summary>
''' <param name = "subItem">sub-item whose data are being edited</param>
Public Sub SetData(ByVal subItem As BetterListViewSubItem) Implements IBetterListViewEmbeddedControl.SetData

    subItem.Text = LabelText

End Sub

These method are trivial since we need not to do any data conversions (the only conversion here is lowering the case of edited text in the LabelText getter).

The last method contained in the interface is SetSize method, which needs not to be implemented (the body can be kept empty). You implement this method only if you need to adjust control's size when label edit starts.

The constructor should be implemented like this:

C#

/// <summary>
///   Initializes a new instance of the <see cref = "TextBoxEmbeddedControl" /> class.
/// </summary>
public TextBoxEmbeddedControl()
{
    AcceptsReturn = true;
    CausesValidation = false;
}

Visual Basic

''' <summary>
'''   Initializes a new instance of the <see cref = "TextBoxEmbeddedControl" /> class.
''' </summary>
Public Sub New()

    AcceptsReturn = True
    CausesValidation = False

End Sub

The AcceptsReturn property is set to true because we will handle the ENTER key (and raise RequestAccept event appropriately).

The CausesValidation property is set to false because it is a good practice in this situation.

Both input and output data are validated in the IBetterListViewEmbeddedControl implementation and validation of some third-party controls can prevent whole form with the control from closing.

The last thing we implement is handling of the ENTER key for accepting the data and the ESCAPE key for cancelling:

C#

protected override void OnKeyDown(KeyEventArgs e)
{
    if (e.KeyCode == Keys.Enter &&
        RequestAccept != null)
    {
        RequestAccept(this, EventArgs.Empty);

        e.Handled = true;

        return;
    }

    if (e.KeyCode == Keys.Escape &&
        RequestCancel != null)
    {
        RequestCancel(this, EventArgs.Empty);

        e.Handled = true;

        return;
    }

    base.OnKeyDown(e);
}

Visual Basic

Protected Overrides Sub OnKeyDown(e As KeyEventArgs)

    If e.KeyCode = Keys.Enter AndAlso RequestAccept IsNot Nothing Then

        RequestAccept(Me, EventArgs.Empty)

        e.Handled = True

        Return

    End If

    If e.KeyCode = Keys.Escape AndAlso RequestCancel IsNot Nothing Then

    RequestCancel(Me, EventArgs.Empty)

    e.Handled = True

    Return

    End If

    MyBase.OnKeyDown(e)

End Sub


It is a common good practice to implement interfaces explicitly. The sample implementation is implicit for the sake of better readability. Embedded controls implemented in BetterListView.dll are implemented implicitly (and marked virtual) to allow for being inherited (e.g. MyCustomControl : BetterListViewEmbeddedControl) and you may possibly want to override any part of the interface implementation.



Implementing IBetterListViewEmbeddedControlExtended

The extended interface has currently only one method called RequestEndEdit. This method can be called by the Better ListView, when it asks the control whether it is ready to end editing. The control can return a boolean value (true - continue EndEdit, false - refuse to end editing). There are many situations when the label editing is terminated (e.g. scrolling the control, selecting items...) and terminating the label edit is not always wanted (this is a case of System.Windows.Forms.DateTimePicker control, which sometimes behaves as being transparent for mouse clicks and thus being closed because of click-through on the Better ListView client area - the RequestEndEdit method fixes such possible behavior of third party controls).

Sample Source Code

Form with Better ListView containing some columns and items:

C#

/// <summary>
///   Shows embedding of custom controls into Better ListView.
/// </summary>
internal sealed partial class EmbeddedControlSampleForm : Form
{
    /// <summary>
    ///   Initializes a new instance of the <see cref = "EmbeddedControlSampleForm" /> class.
    /// </summary>
    public EmbeddedControlSampleForm()
    {
        InitializeComponent();

        this.listView.BeginUpdate();

        this.listView.Columns.AddRange(new[]
                                       {
                                           new BetterListViewColumnHeader("Document name", 160),
                                           new BetterListViewColumnHeader("Access", 128)
                                       });

        this.listView.Items.AddRange(
            new[]
            {
                new BetterListViewItem(new[] { "hydro-report.pdf", "read" }),
                new BetterListViewItem(new[] { "magnetic_resonance.docx", "read write" }),
                new BetterListViewItem(new[] { "billing forms (2011).zip", "read" })
            });

        this.listView.LabelEditActivation = (BetterListViewLabelEditActivation.Keyboard | BetterListViewLabelEditActivation.SingleClick);
        this.listView.LabelEditModeSubItems = BetterListViewLabelEditMode.CustomControl;

        this.listView.EndUpdate();

        this.listView.RequestEmbeddedControl += ListViewRequestEmbeddedControl;
    }

    private IBetterListViewEmbeddedControl ListViewRequestEmbeddedControl(object sender, BetterListViewRequestEmbeddedControlEventArgs eventArgs)
    {
        if (eventArgs.SubItem.Index == 1)
        {
            return (new DocumentAccessConrol());
        }

        return null;
    }
}

Visual Basic

''' <summary>
'''   Shows embedding of custom controls into Better ListView.
''' </summary>
Partial Friend NotInheritable Class EmbeddedControlSampleForm

    ''' <summary>
    '''   Initializes a new instance of the <see cref = "EmbeddedControlSampleForm" /> class.
    ''' </summary>
    Public Sub New()

        InitializeComponent()

        ListView.BeginUpdate()

        ListView.Columns.AddRange(
            New BetterListViewColumnHeader() { _
                                                 New BetterListViewColumnHeader("Document name", 160),
                                                 New BetterListViewColumnHeader("Access", 128)
                                             })

        ListView.Items.AddRange(
            New BetterListViewItem() { _
                                         New BetterListViewItem(New String() {"hydro-report.pdf", "read"}),
                                         New BetterListViewItem(New String() {"magnetic_resonance.docx", "read write"}),
                                         New BetterListViewItem(New String() {"billing forms (2011).zip", "read"})
                                     })

        ListView.LabelEditActivation =
            (BetterListViewLabelEditActivation.Keyboard Or BetterListViewLabelEditActivation.SingleClick)
        ListView.LabelEditModeSubItems = BetterListViewLabelEditMode.CustomControl

        ListView.EndUpdate()

        AddHandler ListView.RequestEmbeddedControl, AddressOf ListViewRequestEmbeddedControl

    End Sub

    Private Function ListViewRequestEmbeddedControl(ByVal sender As Object,
                                                     ByVal eventArgs As BetterListViewRequestEmbeddedControlEventArgs) _
        As IBetterListViewEmbeddedControl

        If eventArgs.SubItem.Index = 1 Then
            Return (New DocumentAccessConrol())
        End If

        Return Nothing

    End Function

End Class

DocumentAccessControl class used as complex embedded control (see EmbeddedControlSampleForm sample in the provided C# and Visual Basic samples for full source code):

C#

/// <summary>
///   Represents a custom control embeddable in Better ListView.
/// </summary>
[ToolboxItem(false)]
internal sealed partial class DocumentAccessConrol : UserControl, IBetterListViewEmbeddedControl
{
    private const string StringRead = "read";
    private const string StringWrite = "write";

    /// <summary>
    ///   current (edited) label text
    /// </summary>
    public string LabelText
    {
        get
        {
            // convert control's state to label
            if (this.checkBoxRead.Checked &&
                this.checkBoxWrite.Checked)
            {
                return String.Format("{0} {1}", StringRead, StringWrite);
            }

            if (this.checkBoxRead.Checked)
            {
                return StringRead;
            }

            if (this.checkBoxWrite.Checked)
            {
                return StringWrite;
            }

            return String.Empty;
        }
    }

    /// <summary>
    ///   request accepting updated data in BetterListView
    /// </summary>
    public event EventHandler RequestAccept;

    /// <summary>
    ///   request cancelling editing
    /// </summary>
    public event EventHandler RequestCancel;

    /// <summary>
    ///   Initializes a new instance of the <see cref = "DocumentAccessConrol" /> class.
    /// </summary>
    public DocumentAccessConrol()
    {
        InitializeComponent();

        //NOTE: disabling validation prevents form close cancellation
        CausesValidation = false;

        foreach (Control control in Controls)
        {
            control.LostFocus += ControlOnLostFocus;
        }
    }

    /// <summary>
    ///   get data from the specified sub-item in control
    /// </summary>
    /// <param name = "subItem">sub-item whose data are being edited</param>
    public void GetData(BetterListViewSubItem subItem)
    {
        // convert label to control's state
        this.checkBoxRead.Checked = subItem.Text.Contains(StringRead);
        this.checkBoxWrite.Checked = subItem.Text.Contains(StringWrite);
    }

    /// <summary>
    ///   set data from control to the specified sub-item
    /// </summary>
    /// <param name = "subItem">sub-item whose data are being edited</param>
    public void SetData(BetterListViewSubItem subItem)
    {
        subItem.Text = LabelText;
    }

    /// <summary>
    ///   set control size
    /// </summary>
    /// <param name = "subItem">sub-item whose data are being edited</param>
    /// <param name = "placement">placement of the embedded control within sub-item</param>
    public void SetSize(BetterListViewSubItem subItem, BetterListViewEmbeddedControlPlacement placement)
    {
        // keep size of the control unchanged
    }

    private void ControlOnLostFocus(object sender, EventArgs eventArgs)
    {
        //
        // NOTE: this code is needed just for hiding embedded control with sub-controls when user changes active form while label editing
        //
        bool anyFocused = Focused;

        if (anyFocused == false)
        {
            foreach (Control control in Controls)
            {
                if (control.Focused)
                {
                    anyFocused = true;

                    break;
                }
            }
        }

        if (anyFocused == false)
        {
            RequestAccept(this, eventArgs);
        }
    }

    private void ButtonOKClick(object sender, EventArgs e)
    {
        RequestAccept(this, e);
    }

    private void ButtonCancelClick(object sender, EventArgs e)
    {
        RequestCancel(this, e);
    }
}

Visual Basic

''' <summary>
'''   Represents a custom control embeddable in Better ListView.
''' </summary>
<ToolboxItem(False)>
Partial Friend NotInheritable Class DocumentAccessConrol
    Inherits UserControl
    Implements IBetterListViewEmbeddedControl

    Private Const StringRead As String = "read"
    Private Const StringWrite As String = "write"

    ''' <summary>
    '''   current (edited) label text
    ''' </summary>
    Public ReadOnly Property LabelText() As String Implements IBetterListViewEmbeddedControl.LabelText
        Get
            ' convert control's state to label
            If CheckBoxRead.Checked AndAlso CheckBoxWrite.Checked Then
                Return [String].Format("{0} {1}", StringRead, StringWrite)
            End If

            If CheckBoxRead.Checked Then
                Return StringRead
            End If

            If CheckBoxWrite.Checked Then
                Return StringWrite
            End If

            Return [String].Empty
        End Get
    End Property

    ''' <summary>
    '''   request accepting updated data in BetterListView
    ''' </summary>
    Public Event RequestAccept As EventHandler Implements IBetterListViewEmbeddedControl.RequestAccept

    ''' <summary>
    '''   request cancelling editing
    ''' </summary>
    Public Event RequestCancel As EventHandler Implements IBetterListViewEmbeddedControl.RequestCancel

    ''' <summary>
    '''   Initializes a new instance of the <see cref = "DocumentAccessConrol" /> class.
    ''' </summary>
    Public Sub New()

        InitializeComponent()

        'NOTE: disabling validation prevents form close cancellation
        CausesValidation = False

        For Each control As Control In Controls
            AddHandler control.LostFocus, AddressOf ControlOnLostFocus
        Next

    End Sub

    ''' <summary>
    '''   get data from the specified sub-item in control
    ''' </summary>
    ''' <param name = "subItem">sub-item whose data are being edited</param>
    Public Sub GetData(ByVal subItem As BetterListViewSubItem) Implements IBetterListViewEmbeddedControl.GetData

        ' convert label to control's state
        CheckBoxRead.Checked = subItem.Text.Contains(StringRead)
        CheckBoxWrite.Checked = subItem.Text.Contains(StringWrite)

    End Sub

    ''' <summary>
    '''   set data from control to the specified sub-item
    ''' </summary>
    ''' <param name = "subItem">sub-item whose data are being edited</param>
    Public Sub SetData(ByVal subItem As BetterListViewSubItem) Implements IBetterListViewEmbeddedControl.SetData

        subItem.Text = LabelText

    End Sub

    ''' <summary>
    '''   set control size
    ''' </summary>
    ''' <param name = "subItem">sub-item whose data are being edited</param>
    ''' <param name = "placement">placement of the embedded control within sub-item</param>
    Public Sub SetSize(ByVal subItem As BetterListViewSubItem,
                        ByVal placement As BetterListViewEmbeddedControlPlacement) _
        Implements IBetterListViewEmbeddedControl.SetSize

        ' keep size of the control unchanged

    End Sub

    Private Sub ControlOnLostFocus(ByVal sender As Object, ByVal eventArgs As EventArgs)

        '
        ' NOTE: this code is needed just for hiding embedded control with sub-controls when user changes active form while label editing
        '
        Dim anyFocused As Boolean = Focused

        If anyFocused = False Then
            For Each control As Control In Controls
                If control.Focused Then
                    anyFocused = True

                    Exit For
                End If
            Next
        End If

        If anyFocused = False Then
            RaiseEvent RequestAccept(Me, eventArgs)
        End If

    End Sub

    Private Sub ButtonOKClick(ByVal sender As Object, ByVal e As EventArgs) Handles ButtonOK.Click
        RaiseEvent RequestAccept(Me, e)
    End Sub

    Private Sub ButtonCancelClick(ByVal sender As Object, ByVal e As EventArgs) Handles ButtonCancel.Click
        RaiseEvent RequestCancel(Me, e)
    End Sub

End Class