Make a TextBox preview extender provider in C#

[example]

This example shows how to convert the TextBox preview techniques described in the last few posts into an extender provider. The previous examples showed the basic technique, but they were hard-wired to specific TextBox controls so they weren’t very flexible. An extender provider provides easy previewing and validation for any kind of TextBox.

To review the basic process of creating an extender provider, see Make an extender provider that validates required TextBoxes in C#.

This example uses an extender provider named TextBoxPreviewer. The following code shows the provider’s class declaration.

[ProvideProperty("ProvidePreview", "TextBox")]
public partial class TextBoxPreviewer : Component, IExtenderProvider
{
    ...
}

This indicates that the class implements the IExtenderProvider interface and provides a property called ProvidePreview for TextBox controls.

The extender provider intercepts keyboard events and displays a context menu for its client TextBox controls. When the user tries to change a TextBox control’s value, either by using the keyboard or the context menu, the extender provider raises an event called TextChangingDelegate to let the main program preview the new text and cancel the change if desired.

To display a context menu, the extender provider must have access to a ContextMenuStrip to display. The following code shows the provider’s constructors, which call the MakeContextMenu method to build the context menu.

public TextBoxPreviewer()
{
    InitializeComponent();

    MakeContextMenu();
}

public TextBoxPreviewer(IContainer container)
{
    container.Add(this);

    InitializeComponent();

    MakeContextMenu();
}

The following code shows how the MakeContextMenu method builds the context menu.

// This extender's context menu.
private ContextMenuStrip ctxTextBox;
private ToolStripMenuItem ctxUndo, ctxCopy, ctxCut, ctxPaste, ctxDelete;
private ToolStripSeparator ctxSeparator;

// Make the context menu.
private void MakeContextMenu()
{
    this.ctxTextBox = new ContextMenuStrip();
    this.ctxUndo = new ToolStripMenuItem();
    this.ctxSeparator = new ToolStripSeparator();
    this.ctxCopy = new ToolStripMenuItem();
    this.ctxCut = new ToolStripMenuItem();
    this.ctxPaste = new ToolStripMenuItem();
    this.ctxDelete = new ToolStripMenuItem();

    ctxTextBox.Opening += ctxTextBox_Opening;
    this.ctxTextBox.Name = "ctxTextBox";
    this.ctxTextBox.Size = new System.Drawing.Size(108, 120);
    this.ctxTextBox.Items.AddRange(new ToolStripItem[] 
        {
            this.ctxUndo,
            this.ctxSeparator,
            this.ctxCopy,
            this.ctxCut,
            this.ctxPaste,
            this.ctxDelete
        });
    this.ctxUndo.Text = "Undo";
    this.ctxUndo.Click += ctxUndo_Click;
    this.ctxCopy.Text = "Copy";
    this.ctxCopy.Click += ctxCopy_Click;
    this.ctxCut.Text = "Cut";
    this.ctxCut.Click += ctxCut_Click;
    this.ctxPaste.Text = "Paste";
    this.ctxPaste.Click += ctxPaste_Click;
    this.ctxDelete.Text = "Delete";
    this.ctxDelete.Click += ctxDelete_Click;
}

You may notice that this code uses this a lot, which is not my usual style. That’s because I copied this code from the Form1.Designer.cs file in the previous example. The code required a little rearranging because in that file it isn’t all in one place like it is here, but it was easier than building the whole menu structure from scratch.

The following code shows how the extender provider implements the IExtenderProvider interface.

// We can extend TextBoxes only.
public bool CanExtend(object extendee)
{
    return (extendee is TextBox);
}

// The list of our clients.
private List<TextBox> Clients = new List<TextBox>();

// Implement the ProvidePreview extension property.
// Return this client's ProvidePreview value.
[Category("Behavior")]
[DefaultValue(false)]
public bool GetProvidePreview(TextBox client)
{
    // Return true if the client is in the Clients list.
    return Clients.Contains(client);
}

// Set this control's ProvidePreview value.
[Category("Behavior")]
[DefaultValue(false)]
public void SetProvidePreview(TextBox client, bool provide_preview)
{
    if (provide_preview)
    {
        // Add the client to the list.
        if (!Clients.Contains(client))
        {
            // Add the client to the list of clients.
            Clients.Add(client);

            // Add event handlers.
            client.KeyDown += txt_KeyDown;
            client.KeyPress += txt_KeyPress;

            // Attach the ContextMenuStrip.
            client.ContextMenuStrip = ctxTextBox;
        }
    }
    else
    {
        // Remove the client from the list.
        if (Clients.Contains(client))
        {
            // Remove the client from the list of clients.
            Clients.Remove(client);

            // Remove event handlers.
            client.KeyDown -= txt_KeyDown;
            client.KeyPress -= txt_KeyPress;

            // Detach the ContextMenuStrip.
            client.ContextMenuStrip = null;
        }
    }
}

The CanExtend method returns true if the provider can extend a particular object. In this example, the provider can extend an object if that object is a TextBox.

The extender provider stores references to the TextBox controls that it serves in a List<TextBox> named Clients. The GetProvidePreview and SetProvidePreview methods get and set the ProvidePreview property values.

This is one of the stranger features of extender providers. The methods that get and set values for a property named Xxx must be named GetXxx and SetXxx. That’s how the program identifies them.

The GetProvidePreview method simply returns true if the TextBox in question is in the Clients list.

The SetProvidePreview method adds or removes a TextBox from the Clients list.

When it adds a TextBox to the list, it registers event handlers to catch the client’s KeyDown and KeyPress events. It also sets the TextBox control’s ContextMenuStrip property to the ContextMenuStrip that the MakeContextMenu method created. If the user later right-clicks the TextBox, this is the menu that is displayed.

When it removes a TextBox from the client list, the SetProvidePreview method unregisters the TextBox controls KeyDown and KeyPress event handlers and sets its ContextMenuStrip property to null.

The end goal of the extender provider is to raise a TextChanging event that the main program can catch to preview TextBox changes and to reject those changes if necessary. The following code shows how the provider declares this event.

// The event we raise when the TextBox's value is about to change.
public delegate void TextChangingDelegate(TextBox text_box,
    string new_text, ref bool cancel);
public event TextChangingDelegate TextChanging;

This code creates a delegate that represents a method that takes as parameters a TextBox, the new value that the TextBox will have if the changes are accepted, and a Boolean that the main program can set to true to cancel the changes.

In the previous examples, the ShouldCancelTextBoxEvent method called a method named ValueIsValid to decide whether the new value made sense. This example uses the following version of ShouldCancelTextBoxEvent.

private bool ShouldCancelTextBoxEvent(
    TextBox txt, EditType edit_type, char new_char, bool assign_value)
{
    // Get the new value.
    int selection_start;
    string new_value = GetNewTextBoxValue(txt, edit_type,
        new_char, out selection_start);

    // Raise the TextChanging event to see if the value is valid.
    bool cancel = false;
    TextChanging(txt, new_value, ref cancel);

    if (cancel)
    {
        // The new value is invalid. Discard it.
        // (Let the main program decide whether to beep.)
        return true;
    }
    else
    {
        // It's okay. Accept it.
        if (assign_value)
        {
            txt.Text = new_value;
            txt.Select(selection_start, 0);
            return true;
        }
        return false;
    }
}

In this version, the code raises the TextChanging event. The main program tests the new value and sets the cancel parameter to indicate whether the extender provider should cancel the event. After the TextChanging event returns, the ShouldCancelTextBoxEvent allows or stops the event as appropriate.

The final parts of the extender provider deal with handling events when the user presses keys or selects a context menu command. These pieces of code are similar to those used by the previous examples except it’s a little harder to figure out which controls were involved in raising the event.

For example, the following code shows the KeyPress event handler that executes when the user presses a key on a client TextBox.

// Handle normal characters.
private void txt_KeyPress(object sender, KeyPressEventArgs e)
{
    TextBox txt = sender as TextBox;

    // Do nothing for special characters.
    if ((e.KeyChar < ' ') || (e.KeyChar > '~')) return;

    // See if the change is valid.
    e.Handled = ShouldCancelTextBoxEvent(
        txt, EditType.NewCharacter, e.KeyChar, false);
}

In the previous examples, this code used a specific TextBox such as txtInteger or txtFloat. This version uses the sender parameter to figure out which TextBox raised the KeyPress event. After the method figures out which TextBox is involved, the rest of the code is the same as before.

The code that responds to context menu events in this example is also a little different from the code used in the previous examples. Those examples used code hard-wired to work with specific TextBox controls. In this example, the context menu might be associated with several controls so the code must figure out which TextBox to use.

Fortunately the ContextMenuStrip component’s SourceControl property tells which control is displaying the context menu. The following code shows how the context menu’s Opening event handler enables the appropriate context menu items when the user right-clicks on a TextBox.

// Enable appropriate context menu items.
private void ctxTextBox_Opening(object sender, CancelEventArgs e)
{
    ContextMenuStrip ctx = sender as ContextMenuStrip;
    TextBox txt = ctx.SourceControl as TextBox;

    // Undo is enabled if the TextBox has something it can undo.
    ctxUndo.Enabled = txt.CanUndo;

    // Copy and Cut are enabled if anything is selected.
    ctxCopy.Enabled = (txt.SelectionLength > 0);
    ctxCut.Enabled = (txt.SelectionLength > 0);

    // Delete is enabled if anything is selected or there
    // is a character after the insertion point to delete.
    ctxDelete.Enabled =
        ((txt.SelectionLength > 0) ||
         (txt.SelectionStart < txt.Text.Length));

    // Paste is enabled if the clipboard contains text.
    ctxPaste.Enabled = Clipboard.ContainsText();
}

First the code gets the ContextMenuStrip object from the event’s sender parameter. It then uses that object’s SourceControl property to get the TextBox that the user right-clicked to open the context menu. After this point the code is similar to the code used in the previous examples.

Just as the context menu’s code must figure out which TextBox was right-clicked, so too must the context menu item event handlers. The following code shows how the Copy menu item’s Click event handler does this.

private void ctxCopy_Click(object sender, EventArgs e)
{
    TextBox txt = ctxTextBox.SourceControl as TextBox;
    Clipboard.Clear();
    Clipboard.SetText(txt.SelectedText);
}

This code uses the extender provider’s ctxTextBox variable to get the ContextMenuStrip. It then uses that object’s SourceControl property to get the TextBox that was right-clicked. After that the code is similar to the previous version.

Download the example to see how the rest of the extender provider’s code works. It’s very similar to the code used in the previous example so it isn’t repeated here.

With all of the extender provider pieces in place, all that remains is the main program. After building the project, I added a TextBoxPreviewer to the form at design time. I then set the ProvidePreview on textBoxPreviewer1 property for the two TextBox controls to true so the provider would serve them.

Next I used the Properties window to create a TextChanging event for the extender provider and added code to make it look like the following.

// Validate a TextBox's new value.
private void textBoxPreviewer1_TextChanging(
    TextBox text_box, string new_text, ref bool cancel)
{
    // If the value is blank, just return and let it happen.
    if (new_text.Length == 0) return;

    // If the value doesn't end in a digit, add a 0.
    if (!char.IsDigit(new_text, new_text.Length - 1)) new_text += '0';

    // See which TextBox this is.
    if (text_box == txtInteger)
    {
        // Make sure this looks like an integer.
        int test_value;
        cancel = !int.TryParse(new_text, out test_value);
    }
    else
    {
        // Make sure this looks like a float.
        float test_value;
        cancel = !float.TryParse(new_text, out test_value);
    }

    // If we set cancel to true, beep.
    if (cancel) System.Media.SystemSounds.Beep.Play();
}

First the code checks the new text’s length and, if the text is blank, the code returns immediately. That leaves parameter cancel set to its initial value false so the event is not canceled and the blank value is allowed.

Next, if the new text doesn’t end in a digit, the code appends the character 0. That turns incomplete values such as “-” and “1.2e” into valid integers and floats as in “-0” and “1.2e0” respectively.

Depending on which TextBox is being modified, the code uses int.TryParse or float.TryParse to see if the new value is valid and sets the cancel parameter accordingly. Finally, if cancel is true, the code plays the beep sound.


Download Example   Follow me on Twitter   RSS feed   Donate




This entry was posted in controls, extensions, user interface and tagged , , , , , , , , , , , , , , . Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *