Click here to Skip to main content
15,885,278 members
Articles / Web Development / ASP.NET
Article

Silverlight Super TextBox (ComboBox, Masked TextBox and More)

Rate me:
Please Sign up or sign in to vote.
4.97/5 (19 votes)
31 Mar 2008CPOL12 min read 167.6K   3.9K   80   17
Supplementing the Silverlight 2.0b1 Controls

Table of Contents

  • Creating a Silverlight Super TextBox (ComboBox, Masked TextBox and More)
  • <li>Introduction</li>
    
    <li>Button</li>
    
    <li>Predefined <code>Button
    s, ComboBox and Erase Inside the TextBox
  • Having a Silverlight Headache!!
  • Formatting Behavior of STextBox
  • Masked Behavior of STextBox
  • Using a Mask or Format Library (and International Features)
  • Validation of Data and Error Handling
  • s Inside the TextBox

Creating a Silverlight Super TextBox (ComboBox, Masked TextBox and More)

This document (with the included source code) demonstrates how to develop a “Super TextBox” control in Silverlight. With the latest 2.0b1 release many controls were included by Microsoft, but some crucial controls are still missing. I hope STextBox will help to fill that void, if only temporarily.

STextBox is still a work in progress. As improvements become available, I will post new versions. I assume Microsoft will eventually release ComboBox and other functionality that will replace this control.

Have fun!

Introduction

Many controls were included with the recent release of Silverlight 2b1, however a few controls were noticeably missing. For instance there is no ComboBox, no Masked TextBox and no TabControl.

Since it will be very hard to develop a business application without such controls, we need to come up with some custom controls.

The control presented here (STextBox) helps out:

Figure 1

By making the code public, I hope others can help to further improve the concept. When such improvements become available, I'll post them. I also have a working Tab Control. Once it is more “mature”, I might post that as well.

Selectively I'll explain some of the features, workarounds and problems I encountered while creating this control. (Please review the source code and code samples for complete information.)

Where possible, I'll refer to links that helped me out and/or other people who came up with suggestions or sample code. Some of the code is from the Microsoft source code, please note their license requirement (it is in the files to which it applies):

<small>// Copyright © Microsoft Corporation.
// This source is subject to the Microsoft Source License for Silverlight Controls(March 2008 Release).
// Please see http://go.microsoft.com/fwlink/?LinkID=111693 for details.</small>

On my personal code I have no license requirement. Please use as you see fit!
I'll take credits but accept no responsibility for failures. (What’s new, right?)

Just as an example, for the longest time I have searched for a way to have the client display a byte array as an Image (using WCF, we receive an Image as a byte[] from the SQL database). That seems like a simple task, and while Image URLs work just fine displaying a JPG from a byte[] is next to impossible in ASP.NET (we used an HttpHandler –ashx-, but I just never appreciated the extra trip to the server).

A few days ago I figured out how it is done in Silverlight 2.0 (I simply bind ImageSource to a slightly customized property):

C#
public ImageSource ThumbImage
{
    get
    {
        if (Thumb == null)
            return null;
        if (Thumb.Bytes.IsEmpty())
            return null;

        BitmapImage bitmap = new BitmapImage();
        bitmap.SetSource(new MemoryStream(Thumb.Bytes));
        return bitmap;
    }
}

I frequently use extension methods (like .IsEmpty() above). When you download the source code, you'll find them in the file SLHelpers.cs. After all, why type me.Visibility = Visibility.Visible when instead I can type me.Show().

Silverlight makes it easier to develop your own controls, so I spent a few days filling the gap in my “control library”. Microsoft was nice enough to release a mix08 source control library so we can peek at the way they created many of the standard controls (TextBox and other basic elements are not included).

The first thing I noticed was that Microsoft includes a Watermarked TextBox that actually does more than just provide a Watermark. It allows you to disable a TextBox (see top) and it has a FocusVisual so you can better see which TextBox has focus:

Figure 2

However, strangely enough you cannot inherit your control from Watermarked TextBox for one major reason. The constructor calls a method SetStyle which loads the XAML code from the assembly and creates the control. SL2.0b1 does NOT allow you to load a Style more than once, so with a Style already loaded by the Watermarked TextBox constructor, there is not much you can do.

For this reason, I have made a copy of this control (labelled SWatermarkedTextBox) with one minor but crucial change in it:

C#
protected virtual void SetStyle()
{

}

Now we can override SetStyle, and enjoy the features of Enable/Disable, Watermark and focus at “no extra cost”:

C#
public class STextBox : SWatermarkedTextBox
{

}

It is also interesting to see that WatermarkedTextBox does NOT use generic.xaml. Why would you want all your XAML code for controls to be in the same file anyway?

Included with the sample code is a simple Page.XAML (notice the g: reference for the STextBox library):

XML
<UserControl x:Class="SilverlightTextBox.Page"
    xmlns="http://schemas.microsoft.com/client/2007"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:g="clr-namespace:SQControls;assembly=SQControls"
    Width="400" Height="300">
        <Grid x:Name="LayoutRoot" Background="#FF3C5DA2">
        </Grid>
</UserControl>

Within the UserControl you could also have a Canvas:

XML
<Canvas x:Name="LayoutRoot" Background="#FF3C5DA2">
</Canvas>

Figure 2 is the result of these lines:

XML
<g:STextBox x:Name="Test1" Width="150" Height="25" Margin="5" FontSize="15"
    Text="Some text" IsEnabled="False"/>

<g:STextBox x:Name="Test2" Width="150" Height="25" Margin="5" FontSize="15"
    Watermark="Required field!" />

Buttons Inside the TextBox

I have always enjoyed the 3rd party controls that would enable me to include all kinds of Buttons inside of the TextBox. Unlike Button which has a Content property and hence can be filled with anything you like, TextBox only has a Text property and does not allow you to fill the TextBox with other controls.

In XAML, it looks like this:

XML
<g:STextBox x:Name="Test6" Width="150" Height="25" Margin="5"
    FontSize="15" Text="Sample" >

   <g:STextBox.Content>
        <StackPanel Orientation="Horizontal" Width="40" HorizontalAlignment="Left">
          <Button x:Name="BLookup" Width="20" Height="20" Content="…" />
          <Button x:Name="BHelp" Width="20" Height="20" Content="?" />
        </StackPanel>
   </g:STextBox.Content>

</g:STextBox>

And it displays like this:

Figure 3

One of the (failed) attempts I made was to have that collection respond to the usual Mouse events on the Buttons itself (MouseOver, MouseClick).

C#
buttonElement.IsHitTestVisible = true;

buttonElement.IsTabStop = tabStop;
if buttonElement.Content is Control)
{
    (buttonElement.Content as Control).IsHitTestVisible = true;
    (buttonElement.Content as Control).IsTabStop = tabStop;
}
Panel pnl = buttonElement.Content as Panel;
if (pnl != null)
{
    foreach
        (UIElement ctrl in pnl.Children)
    {
        if (ctrl is Control)
        {

            (ctrl as Control).IsHitTestVisible = true;

            (ctrl as Control).IsTabStop = tabStop;
        }
    }
}

That works fine to disable the TabStops on these buttons, but does nothing to make the content come alive (HitTestVisible). So as a workaround, I handle mouse-over and mouse-click events inside the STextBox control.

XML
<g:STextBox x:Name="Test6" Width="150" Height="25" Margin="5" FontSize="15"
    Text="Sample" ContentClick="Test6_ContentClick" >
<………>

Hovering over one of the Buttons will change the mouse to “Hand” and when clicked, the ContentClick event will be executed. For example:

C#
private void Test6_ContentClick(object sender, RoutedEventArgs e)
{
    if (sender == BLookup)
        "Lookup goes here".Alert();
    if (sender == BHelp)
        "Help goes here".Alert();
}

When adding a ListBox or an error indicator to the TextBox (as demonstrated below), we run into the same issue. The graphics work, but there is no keyboard or mouse control. It is for this reason that ListBox and error indicator are added to the parent control collection. We go up the control tree to find a Canvas or Grid where we can add these dynamic parts of STextBox.

Adding these controls to the parent of STextBox works out fine, but it means we have to manually position the ListBox (or error indicator). Considering StackPanels, Grids, Margins etc., that is not an easy task. It works out in most cases I tested, but more complicated scenarios might fail to do proper positioning.

Predefined Buttons, ComboBox and Erase Inside the TextBox

The following XAML code demonstrates the predefined Button to erase the content of the TextBox (EraseButtonName property):

XML
<g:STextBox x:Name="Test3" Width="150" Height="25" Margin="5" FontSize="15"
    Text="Sample" EraseButtonName="ButtonErase" >
   <g:STextBox.Content>
        <Button x:Name="ButtonErase" Width="20" Height="20"
              HorizontalAlignment="Left" Content="X" />
   </g:STextBox.Content>
</g:STextBox>
Figure 4

The following XAML code demonstrates the predefined Button for a DropDownList (ComboBox):

XML
<Canvas>
<g:STextBox x:Name="Test5" Width="150" Height="25" Margin="5" FontSize="15"
    Text="Sample" DropDownButtonName="ButtonDD" DropDownPosition="BottomRight">
   <g:STextBox.Content>
        <Button x:Name="ButtonDD" Width="20" Height="20" Content="6"
             FontFamily="Webdings" HorizontalAlignment="Right"/>
   </g:STextBox.Content>
   <g:STextBox.ListBoxContent>
        <ListBox x:Name="DDBox" Height="100" >
               <ListBoxItem Content="Option1"/>
               <ListBoxItem Content="Option2"/>
               <ListBoxItem Content="Option3"/>
               <ListBoxItem Content="Sample"/>
               <ListBoxItem Content="Option4"/>
               <ListBoxItem Content="Option5"/>
               <ListBoxItem Content="Option6"/>
        </ListBox>
   </g:STextBox.ListBoxContent>
</g:STextBox>
</Canvas>
Figure 5

Some additional features:

  • DropDownPosition, allows you to set where the ListBox will show in relationship to the TextBox (Top or Bottom and if it does not have the same width aligned to the right or the left).
  • DropDownKey, sets the name of the hot-key that will show the ListBox (defaults to Down-key).
  • StrictDropDown, when set to True will make the TextBox entry ReadOnly so the user can ONLY select values from the list.

For the most part, keyboard control works as you would expect.

Issues/Problems

There are a number of issues in the current setup:

Setting Selected Item in ListBox

I was initially unable to make the correct initial item “Selected” in the dropdown. I fixed this with a Timer (the code is still in there since it is interesting to see this in Silverlight), but later I found a fix in SelectedIndexWorkAround.

Using the Mouse-Wheel

Using the mix08 samples from Mike Harsh, it was not very hard to enable the ListBox for scrolling with the mouse-wheel. An embedded JavaScript file takes care of the interaction and STextBox syncs the mouse-wheel events with the Listbox (if it has focus!).

Tab Sequence on Dropdown ListBox

The weird thing with the current TabStop implementation, is that Silverlight attempts to Tab to controls that are not really visible. It is therefore important to set IsTabStop to false on anything that is not visible. For this reason, my extension methods on visibility (.Hide(), .Show(), SetVisibility()) all call...

C#
private static void SyncTabStop(UIElement me, bool vis)
{
    // for SL2.0b1, Collapsed with IsTabStop is a big issue...
    if (me == null) return;
    if (me is Control)
    {
        (me as Control).IsTabStop = vis;
    }
    else if (me is Border)
    {
        SyncTabStop((me as Border).Child, vis);
    }
    else if (me is Panel)
    {
        if (vis)
            (me as Panel).Children.StartTabKey();
        else
            (me as Panel).Children.StopTabKey();
    }
}

... where StartTab and StopTab cycle through all children to set the IsTabStop property.

Sadly once I make the ListBox visible I am unable to control TabStop behavior for the ListBox itself. Hence when you use Tab or Shift-Tab on the “popup” ListBox focus jumps to the addressbar or some other strange place. It would be nice if I could programmatically catch the Tab behavior and control it properly.

Setting Focused Item in ListBox

This might sound similar but is not. When you look carefully at Figure 5, you see that the ListBox item “Sample” is selected, but the item “Option 1” has focus. I traced that to an oversight in the ListBox code where _tabOnceActiveElement is stubbornly reset to the first item no matter what you select. Only an actual mouse-click or keyboard event (up/down) can change this.

So the strange behavior is that you open the listbox with keydown, but subsequent keyboard control will be based on the first item and not on the selected item.

Now I also had a desire to retrieve the actual ListBoxItem. That seems easy but if you DataBind the ListBox to some collection, the items of the ListBox are of your collection type and not of type ListBoxItem (the latter one comes in handy for visual effects, focus etc.).

Thirdly I would like to get the ElementScrollViewer (for instance to adjust the Height of the ListBox based on the number of items).

I did figure out the code that I would like to add to the ListBox to fix various features:

C#
public void Set_tabOnceActiveElement()
{
    _tabOnceActiveElement = GetListBoxItemForObject(Items[SelectedIndex]);

    if (_tabOnceActiveElement!=null)
        _tabOnceActiveElement.Focus();
}
public ListBoxItem Get_ListBoxItemForObject(object value)
{
    return GetListBoxItemForObject(value);
}

public ScrollViewer Get_ElementScrollViewer()
{
    return ElementScrollViewer;
}

Having a Silverlight Headache!!

Failed Attempt 1

Normally I would have been able to make these simple adjustments by using reflection, and initially I attempted to do just that. BUT in Silverlight, reflection is not allowed to access any private/internal member and/or to Invoke any such method. That was much to my surprise but is further outlined here:

In Silverlight, reflection cannot be used to access private types and members. If the access level of a type or member would prevent you from accessing it in statically compiled code, then you cannot access it dynamically by using reflection.

That, to me is just very inconvenient and simply not in the .NET spirit. I can only assume there is some underlying major security risk that requires this limitation. It would not have been so bad if the Microsoft controls like ListBox did not mark almost ALL members at the lowest visibility (mostly internal due to unit testing).

Failed Attempt 2

I subsequently tried to modify the Mix08 source code and create my own version of System.Windows.Control. That caused many undesirable side effects (vs XAML previewer died) and I am not so sure the Mix08 source code is completely identical to the released version.

Failed Attempt 3

Since I was successful with making a copy of the WatermarkedTextBox, and reuse it after some minor changes (name and some resource strings), why not do that for ListBox….

However ListBox does not work by itself, it is dependent on many of the other controls and could not be moved into my own control assembly without its buddies. After moving over what seems like half the control library, I gave up.

Formatting Behavior of STextBox

By just going through some normal form scenarios, I was quickly stuck on the next issue. I had several fields which would not display the way I liked.

Formatting With a Value Converter

On an enum value with a dropdown ListBox, using a value converter worked fine. Isn't it great to use LINQ like this:

C#
public class DBCompanyEnumConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter,
        System.Globalization.CultureInfo culture)
    {
        if (CompanyEditor.Settings != null)
        {
            string name =  (from e in CompanyEditor.Settings.CompanyEnums
                where e.DBCompanyEnumId == (byte)value
                select e.DBCompanyEnumName).FirstOrDefault();
            return name;
        }
        return "Type " + value.ToString();
    }
    public object ConvertBack(object value, Type targetType, object
        parameter, System.Globalization.CultureInfo culture)
    {
        int Id = (from e in CompanyEditor.Settings.CompanyEnums
            where e.DBCompanyEnumName == (string) value
            select e.DBCompanyEnumId).FirstOrDefault();
        return (byte)Id;
    }
}

BTW, in XAML you hook it up like this:

XML
<UserControl.Resources>
        <c:DBCompanyEnumConverter x:Key="CTypeFormatter"/>
    </UserControl.Resources>

<……>

    <g:STextBox x:Name="CompanyType" Text="{Binding CompanyType, Mode=TwoWay,
        Converter={StaticResource CTypeFormatter} }"
        BorderThickness="0"  DropDownButtonName="TypeDropDown"
        StrictDropDown="True">
      <g:STextBox.Content>
        <g:SButton x:Name="TypeDropDown" Width="16" Height="16" Content="6"
        FontFamily="Webdings" HorizontalAlignment="Right"/>
      </g:STextBox.Content>
      <g:STextBox.ListBoxContent>

<ListBox Width="120" Height="140" DisplayMemberPath="DBCompanyEnumName">
</ListBox>
</g:STextBox.ListBoxContent>
</g:STextBox>

Formatting with a String.Format Expression

Using standard .NET formatting, you can do a lot (when you use the Format property bind to Value instead of Text!!). In XAML, it looks like this (and binds to a double Markup):

XML
<g:STextBox x:Name="Markup" Format="###.##%" Value="{Binding Markup,
    Mode=TwoWay}" BorderThickness="0" HorizontalAlignment="Right" Width="90"/>
Figure 6
C#
private void OnValueChanged()
{
    //
    also See OnLostFocus for reverse mapping...
        if
            (Value == null)
            base.Text = "";
        else if (!String.IsNullOrEmpty(_Format))
        {
           // tips see http://blog.stevex.net/index.php/2007/09/28/string-formatting-faq/
           // and http://john-sheehan.com/blog/index.php/net-cheat-sheets/
            base.Text = String.Format(TextBoxCulture,"{0:" + _Format + "}", Value);
        }
        else
            base.Text = Value.ToString();
}

When Focus is lost, STextBox attempts to do the reverse mapping (from Text and Format string to the Value object).

C#
if (!String.IsNullOrEmpty(_Format))
{
    //
    here handle any special "DEFORMATTING" tricks so the generic
        Convert.ChangeType will work...

        if (_Format.Contains("#%") || _Format.Contains("9%") || _Format.Contains("#'%")
             || _Format.Contains("9'%")) // % sign optional
        {
            input = input.Replace("%", "");
            <....>
        }
}
<validation code>
if (error == null)
{
    Value = Convert.ChangeType(input,
        Value.GetType(), TextBoxCulture);
    //
    Value setter can also throw validation errors !!
}

Masked Behavior of STextBox

Combined with a Value (see Formatting Behavior) or with a Text property binding you can use a Mask Value. When you bind to Value, as opposed to Text, STextBox will bind to a CLEAN version of the Text (where all formatting characters are removed).

XML
<g:STextBox x:Name="Test4" Width="150" Height="25" Margin="5" FontSize="15"
    Text="8001234567" Mask="(000)000-0000" />

Displays like this:

Figure 7

Mask characters are almost identical to the characters Windows Masked TextBox (see the comments in file TextMaskController.cs). One exception, add a “*” in front of the mask and a user can type the * character followed by “free text”. After all, sometimes the preselected formatting just does not fit.

XML
<g:STextBox x:Name="Test4" Width="150" Height="25"
    Margin="5" FontSize="15" Text="8001234567" Mask="(000) 000-0000" />
Figure 8

In addition, you can supply a MaxLength. Note however that you probably need a MaxLength one character more that the Mask (a Silverlight TextBox is always in “insert” mode and hence we need one extra character).

Using a Mask or Format Library (and International Features)

While a mask works fine, when we handle different languages/countries, it does not work out (of course). Hence instead of Mask you can do MaskLib:

XML
<g:STextBox x:Name="Zip" Width="150" Height="25" Margin="5" FontSize="15"
    MaskLib="ZIP" Value="123457890" MaxLength="11" BorderThickness="0"/>
Figure 9

TextBox has a static CultureInfo:

C#
public static CultureInfo TextBoxCulture = CultureInfo.CurrentUICulture;

If for demonstration we change that to TextBoxCulture = new CultureInfo("nl-NL");

Figure 10

Voila, we get to see a Dutch (NL for Netherlands) formatted ZIP code (4 numbers and 2 letters). The code that makes this happen is in ControlHelpers.cs:

C#
private static string MaskZIP(string country)
{
    switch (country)
    {
        case "nl": return "0000-LL";
        default: return "00000-9999"; // USA
    }
}

Of course my library of Masks is very rudimentary, but it will grow (help is welcome)!

Validation of Data and Error Handling

All that is left is validation of the data.

It is okay for a ValueConverter or a Setter property to throw an error, it will work very similar to validation. However a more formal validation is provided as a library. Simply type Validate=”” in XAML and an enum will pop up with possible validations.

For now, this is a short list (notice how SmallMoney checks to see if a double is valid in the SQL SmallMoney range and number of decimals):

C#
public enum Validate: byte
   {
       None,
           EmptyString,
           Email,
           Password,
           SmallMoney
   }

So we could provide the following XAML:

XML
<g:STextBox x:Name="Zip" Width="150" Height="25" Margin="5" FontSize="15"
   MaskLib="ZIP" Value="123457890" MaxLength="11" BorderThickness="0"
   Validate="EmptyString"/>

Now typing a blank value results in:

Figure 11

Hovering over the “error” icon will popup a tooltip explaining that input for this field is required.

In addition, there is an ErrorConditionEventHandler (test condition or NullOrEmpty for an error reset).

C#
public delegate void ErrorConditionEventHandler(string condition);
    public event ErrorConditionEventHandler ErrorCondition;

The event can be useful to let a form know there is an error (and for instance disable the “Save” button).

History

  • March 26, 2008: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United States United States
Robert has over 15 years of experience as a Senior Software Developer and Project Manager (Scrumm Certified). He has a Master Degree in Computer Science and lives in sunny Orlando, Florida.

His current interest is in Silverlight technology for 2 major reasones:

1. Breaking the barrier. Allow heavy duty desktop applications to run over the internet without sacrificing performance or UI.

2. Develop applications that can run from a web server and from the desktop, with only minor internal changes. Why develop twice if you can do it once?

His Target is to complete a complex catalog/project program shortly after Microsoft delivers beta-2 (and a commercial go-live license).

He can be reached by email: Robert@LindersUSA.com.

Comments and Discussions

 
GeneralGreat Thank you!!! Pin
CodeNinga4-Oct-10 22:30
CodeNinga4-Oct-10 22:30 
Generalerror Pin
bharathkumar_cse11-Feb-10 17:41
bharathkumar_cse11-Feb-10 17:41 
GeneralThanks! :) Pin
Member 381426910-Nov-09 1:49
Member 381426910-Nov-09 1:49 
Generalcode not working Pin
Member 2080853-Aug-09 20:46
Member 2080853-Aug-09 20:46 
QuestionAnything happening on this? Pin
Gary E Paul14-Jan-09 14:59
Gary E Paul14-Jan-09 14:59 
NewsSilverlight 2 beta 2 Pin
Robert Linders21-Aug-08 3:39
Robert Linders21-Aug-08 3:39 
Questionwhen it can working in beta2? Pin
ForrestZhang10-Jun-08 16:43
ForrestZhang10-Jun-08 16:43 
Questiondoes it work in S2 Beta 2? Pin
diegolaz10-Jun-08 2:52
diegolaz10-Jun-08 2:52 
AnswerRe: does it work in S2 Beta 2? Pin
Sean Aitken10-Jun-08 10:50
Sean Aitken10-Jun-08 10:50 
AnswerRe: does it work in S2 Beta 2? Pin
Adam Wawrzyniak19-Jun-08 7:59
Adam Wawrzyniak19-Jun-08 7:59 
Questionhow to use ItemsSource property for combobox Pin
Manoj G19-May-08 13:21
Manoj G19-May-08 13:21 
QuestionFormat Pin
Member 38779038-May-08 12:33
Member 38779038-May-08 12:33 
GeneralDropDown - BringToFront Pin
gourmettt16-Apr-08 14:28
gourmettt16-Apr-08 14:28 
GeneralRe: DropDown - BringToFront Pin
Sean Aitken20-May-08 5:44
Sean Aitken20-May-08 5:44 
Questionuse in WPF application? Pin
Jason Law9-Apr-08 18:11
Jason Law9-Apr-08 18:11 
GeneralThanks! Pin
Dmitri Raiko5-Apr-08 0:17
Dmitri Raiko5-Apr-08 0:17 
GeneralNice work! Pin
User 2710091-Apr-08 16:49
User 2710091-Apr-08 16:49 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.