Browse by Tags

All Tags » textblock   (RSS)

The Problem

A common UI pattern is to expose text in a read-only container, then swap it for an editable input box based on a command. There are multiple approaches to doing this (including just changing the style of the same container). Swapping between a read-only TextBlock to a TextBox is easy enough, but what if you want to also focus and select the TextBox so the user can simply begin typing? And what if the UI elements are nestled deep with in a data template so there is no straightforward way to reference them?

A Solution

I say, "a solution" because there are probably other ones, but this is how I recently tackled the problem.

Unique Identifier

First, I figured no matter how nested the text box would be, it most likely is data bound to some data element. So, in order to uniquely identify the "transaction" I could expose a unique identifier on the bound object. Assume the bound field is "Name":

public string NameIdentifier { get; private set; }

...

// constructor
NameIdentifier = Guid.NewGuid.ToString();

The Message

Using the Event Aggregator pattern, I created a message payload specifically for the "message" that the text item should receive focus:

public class TextFocusEvent
{
    public TextFocusEvent(string identifier)
    {
        Identifier = identifier;
    }
    public string Identifier { get; set; }
}

In the "edit command" we can now publish our message that the text box should receive focus (notice I'm using the DelegateCommand from Prism):

EditCommand = new DelegateCommand<object>(
    obj =>
        {
            if (!EditCommand.CanExecute(obj)) return;

            _oldName = _name;
            IsInEditMode = true;
            EventAggregator.Publish(new TextFocusEvent(NameIdentifier));
        },
    obj => !IsInEditMode);

In this case, you can infer we have an IsInEditMode flag exposed, which we can use to bind the visibility of the TextBlock and TextBox to swap them out. The event saves the old name so if the user cancels, it can be put back. We've just published an event to let the world know that text box deserves some focus. Now let's catch it!

The Behavior

I decided to go with a behavior that could subscribe to the message. Because my event aggregator is based on Reactive Extensions (Rx), instead of being a straight event subscription, it actually returns IObservable, which can be filtered using LINQ. This way an attached behavior can simply listen for the specific identifier it is "tuned" to. We want to databind the identifier because it is generated by the view model, so we expose that property as a dependency property. Here's the behavior:

public class TextBoxFocusListenerBehavior : Behavior<TextBox>
{
    [Import]
    public IEventAggregator EventAggregator { get; set; }

    public static readonly DependencyProperty IdentifierProperty =
            DependencyProperty.Register("Identifier", 
            typeof (string), 
            typeof (TextBoxFocusListenerBehavior),
            new PropertyMetadata(string.Empty));

    private IDisposable _listener;

    public string Identifier
    {
        get { return GetValue(IdentifierProperty).ToString(); }
        set { SetValue(IdentifierProperty, value);}
    }

    protected override void OnAttached()
    {
        if (DesignerProperties.IsInDesignTool)
        {
            return;
        }

        if (EventAggregator == null)
        {
            CompositionInitializer.SatisfyImports(this);
        }

        if (EventAggregator != null)
        {
            var query = from evt in EventAggregator.GetEvent<TextFocusEvent>()
                        where evt.Identifier.Equals(Identifier)
                        select true;

            _listener = query.Subscribe(evt =>
                                            {
                                                if (AssociatedObject.Focus())
                                                {
                                                    AssociatedObject.SelectAll();
                                                }
                                            });
        }

        base.OnAttached();
    }

    protected override void OnDetaching()
    {
        if (_listener != null)
        {
            _listener.Dispose();
        }
        base.OnDetaching();
    }
}

So what's going on? First, we are importing the global event aggregator (we know this fires on the UI thread, so I'm not using a mutex to check to see if I need to satisfy the imports and request the reference from MEF). To stay design-time friendly, we don't try to compose if we're in design mode.

When the behavior is attached, the subscription is made for the event. Notice, however, that a filter is being used to filter only the identifier we are interested in. When a new event is pushed to us, we simply set the focus and auto-select the text. This has the effect of highlighting the text box so the user can begin typing right away. When the behavior is detached, we dispose of the subscription.

The XAML

Now we can put it all together and attach the behavior in our XAML:

<TextBox Text="{Binding Name, Mode=TwoWay}">
    <i:Interaction.Behaviors>
             <behaviors:TextBoxFocusListenerBehavior Identifier="{Binding NameIdentifier}"/>
    </i:Interaction.Behaviors>
</TextBox>
<HyperlinkButton Content="edit" Command="{Binding EditCommand}"/>

I've left out the nuances of the TextBlock and related code to swap into/out of view, but you get the point ... now, even if the text box is buried within a set of data templates, simple data-binding gives us the way to tie the edit event with the focus behavior. Of course, the event aggregator is a generic approach: you could also create a more strongly typed message contract between the behavior and the event.
Jeremy Likness

One common complaint I see regarding Silverlight is the inability to include inline hyperlinks. While Silverlight does provide a HyperlinkButton, you cannot do something simple like:


This is some text. This is <a href="http://www.wintellect.com/">a hyperlink</a>, in case you were wondering. 

There are a few solutions available via third-party controls, but it's a good exercise to understand the underlying fundamentals of the rendering engine, controls, and objects that make up a TextBlock. In this article, we'll set out on a quest to be able to take a simple set of text, formatted for a text block, and generate inline links.

The first step is to simply open your solution, start a new Silverlight 3 application, and get rolling with some text. The easiest way to grab text is to visit Lorem Ipsum, where you can request however much text you need and it will generate the paragraphs for you. I just grabbed a few sentences and set the width of my control to 200 so I could show the text wrapping. The final markup looks like this (I called my project "SLHyperlink"):

<UserControl x:Class="SLHyperlink.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
   mc:Ignorable="d" d:DesignWidth="640" d:DesignHeight="480"
   Width="200">
      <Grid x:Name="LayoutRoot">
        <TextBlock TextWrapping="Wrap">Lorem ipsum dolor sit amet, consectetur adipiscing elit. 
            Aenean eget turpis id purus tempor tincidunt porttitor eu mi. 
            Vestibulum tincidunt odio quis nibh feugiat faucibus. 
            Fusce rhoncus tristique mi non posuere. 
            Nunc sit amet velit magna.
  </Grid>
</UserControl>

When I run the solution, this is what appears:

Lorem Ipsum

Now, we can get a little fancier. If you are familiar with the TextBlock control, you know you can mix and match different styles of text within the control, and even provide line breaks. You do this using the Run and the LineBreak markup. Let's spruce up the text a bit. I made the font size larger, added a few runs and a line break, and changed the width of the control to 300 to accommodate the larger text:


<TextBlock TextWrapping="Wrap" FontSize="20"><Run FontFamily="Times New Roman">Lorem ipsum dolor sit amet, consectetur adipiscing elit.</Run> 
            Aenean eget turpis id purus tempor tincidunt porttitor eu mi. 
            <Run Foreground="Cyan">Vestibulum tincidunt odio quis nibh feugiat faucibus.</Run> 
            Fusce rhoncus tristique mi non posuere.<LineBreak/>
            Nunc sit amet velit magna.</TextBlock>

The result is this:

Lorem Ipsum

Anatomy of a TextBlock

Now that we have the inline text, we can start to dissect the classes. There are a few tools at our disposal. First, we can go into the Object Browser in Visual Studio and search for TextBlock. We find out that it inherits directly from FrameworkElement and is, unfortunately, a sealed class, so we cannot create our own dervied class. There are plenty of text-related dependency properties (font size, font family, font weight, etc) and then a few interesting methods like OnCreateAutomationPeer and the collection of Inlines.

Running RedGate's free .Net Reflector tool, we can generate the shell of the class:


[ContentProperty("Inlines", true)]
public sealed class TextBlock : FrameworkElement
{
    // Fields
    private FontSource _fontSource;
    public static readonly DependencyProperty FontFamilyProperty;
    public static readonly DependencyProperty FontSizeProperty;
    public static readonly DependencyProperty FontStretchProperty;
    public static readonly DependencyProperty FontStyleProperty;
    public static readonly DependencyProperty FontWeightProperty;
    public static readonly DependencyProperty ForegroundProperty;
    private static readonly DependencyProperty InlinesProperty;
    public static readonly DependencyProperty LineHeightProperty;
    public static readonly DependencyProperty LineStackingStrategyProperty;
    public static readonly DependencyProperty PaddingProperty;
    public static readonly DependencyProperty TextAlignmentProperty;
    public static readonly DependencyProperty TextDecorationsProperty;
    public static readonly DependencyProperty TextProperty;
    public static readonly DependencyProperty TextWrappingProperty;

    // Methods
    static TextBlock();
    public TextBlock();
    internal override string GetPlainText();
    protected override AutomationPeer OnCreateAutomationPeer();
    private void UpdateFontSource(FontSource fontSource);

    // Properties
    public FontFamily FontFamily { get; set; }
    public double FontSize { get; set; }
    public FontSource FontSource { get; set; }
    public FontStretch FontStretch { get; set; }
    public FontStyle FontStyle { get; set; }
    public FontWeight FontWeight { get; set; }
    public Brush Foreground { get; set; }
    public InlineCollection Inlines { get; }
    public double LineHeight { get; set; }
    public LineStackingStrategy LineStackingStrategy { get; set; }
    public Thickness Padding { get; set; }
    public string Text { get; set; }
    public TextAlignment TextAlignment { get; set; }
    public TextDecorationCollection TextDecorations { get; set; }
    public TextWrapping TextWrapping { get; set; }
}

What's interesting for us is the Inlines collection. In Silverlight, the System.Windows.Documents.Inline class is summarized as "An abstract class that provides a base for all inline flow content elements." There are exactly two classes that inherit from them, and both of them are sealed: Run and LineBreak. The Run class simply contains a text property:


[ContentProperty("Text", true)]
public sealed class Run : Inline
{
    // Fields
    private static readonly DependencyProperty TextProperty;

    // Methods
    static Run();
    public Run();

    // Properties
    public string Text { get; set; }
}

It turns out if you give your TextBlock a name (x:Name="tbSomething") and then debug, you'll find that the text we entered inside the TextBlock is turned into a collection. The free text goes into a run without additional attributes, so the collection is of runs and line breaks. Not only are these classes sealed, but they inherit directly from DependencyObject, so they are not FrameworkElement (or even UIElement) derived. This means we can't do things like check mouse events or bind to click events.

Instead of creating our own text block control, it appears that the existing has everything we need except the actual hyperlink. After brainstorming for a bit, I decided the easiest way to implement this would be to translate the text block into a wrap panel. A wrap panel does what we want: it allows elements of various sizes to stack together (like text flowing together). If we want an inline hyperlink, we simply need to stack a text block, followed by a navigation button for the hyperlink, followed by another text block. We can separate out the hyperlink by placing it in its own run.

An Extended Run

Even though our Run class is sealed, it derives from Inline which, in turn, is a DependencyObject, so we are allowed to generate attached properties.

The first thing I did was create a static class called RunExtender and add an attached property for a NavigateUrl. This is what I can attach to the run that will become a hyperlink. The code looks like this:


public static class RunExtender 
{  
    public static Uri GetNavigateUrl(DependencyObject obj)
    {
        return (Uri)obj.GetValue(NavigateUrlProperty);
    }
    
    public static void SetNavigateUrl(DependencyObject obj, Uri value)
    {
        obj.SetValue(NavigateUrlProperty, value); 
    }

    public static readonly DependencyProperty NavigateUrlProperty =
        DependencyProperty.RegisterAttached("NavigateUrl",
                                    typeof(Uri),
                                    typeof(RunExtender),
                                    null);
}

Now I can add an inline hyperlink and add the attached navigation property. This won't change anything yet, but it sets us up for the next iteration.

First, I add the namespace to the top of the control:


xmlns:local="clr-namespace:SLHyperlink"

Next, I add some more text to the text block and decorate it with the new attached property:


<TextBlock TextWrapping="Wrap" FontSize="20"><Run FontFamily="Times New Roman">Lorem ipsum dolor sit amet, consectetur adipiscing elit.</Run> 
            Aenean eget turpis id purus tempor tincidunt porttitor eu mi. 
            Here is an <Run local:RunExtender.NavigateUrl="http://www.wintellect.com/">inline hyperlink</Run>.
            <Run Foreground="Cyan">Vestibulum tincidunt odio quis nibh feugiat faucibus.</Run> 
            Fusce rhoncus tristique mi non posuere.<LineBreak/>
            Nunc sit amet velit magna.</TextBlock>

The generated output isn't interesting yet, but no errors so far:

Lorem Ipsum

Warping the WrapPanel

Now things are getting interesting. What I want to do is make my text block a resource, then move it into a wrap panel. Attached properties come to our rescue again, because I can create an attached property for a wrap panel that points to my text block resource. When it is attached, we'll parse the inline collection and build out a new set of children for the wrap panel to render.

We're going to have to move a lot of values from the reference text block to our targets in the wrap panel. The first thing I did was to add some static collections of attributes for the text block, inline element, and a generic control (in the case of the hyperlink button we'll generate). I also want to clean the text as we move it from one to the other, so I set up a regular expression to help strip whitespace. This block looks like:


private static FieldInfo[] _tbInfo = typeof(TextBlock).GetFields(BindingFlags.Static | BindingFlags.Public);
private static FieldInfo[] _rInfo = typeof(Inline).GetFields(BindingFlags.Static | BindingFlags.Public);
private static FieldInfo[] _cInfo = typeof(Control).GetFields(BindingFlags.Static | BindingFlags.Public);
private static readonly Regex _reg = new Regex(@"\s+");

Notice we are just getting the public and static properties from the types.

The clone method I created could probably be refactored and cleaned up several times over, but it does the trick. I want to be able to transfer properties from a source to a target. If the source and target are the same type, we can reference the same dependency properties. If they are different types, we'll need to lookup the dependency property on the target that has the same name as the dependency property on the source. Take a peek at this:


private static void _Clone(DependencyObject src, DependencyObject tgt, FieldInfo[] fi)
{
    _Clone(src, tgt, fi, null);
}

private static void _Clone(DependencyObject src, DependencyObject tgt, FieldInfo[] fi, FieldInfo[] fiTgt)
{
    for (int i = 0; i 

(Yes, I know there is some duplicated code that could be pulled out into another method)

So we essentially have our source and target, and the collection that contains the fields and properties. I only send a second collection in if the target is different.

We begin by iterating all properties on the source. We need to make sure we get a valid reference. I then filter the properties to only move what I'm interested in. For the text block, it's the font, line, and foreground properties, as well as the text alignment and text wrappig. I don't want, for example, to get the "Height" property (we'll let the engine size it for us) and I certainly don't want to move the "Text" property (we'll be iterating the collection of inlines for that).

For the run, it's just font information and the foreground.

If the value is a nested object (for example, a brush), we simply set the value on the target. If the target is a different type, I first iterate the target list of properties to find the property with the same name, then set the object to that.

If it's not a nested object, I get the value, then set the value on the target. Again, if the target is a different type, I have to find the corresponding dependency for the target and then set the value on that.

Now that we've got some nice helpers in place, we can set up a new attached property that points to a text block. Remember, my goal is to take that in, parse it, and output elements into a wrap panel.

Here is the setup:


public static TextBlock GetTargetTextBlock(DependencyObject obj)
{
    return (TextBlock)obj.GetValue(TargetTextBlockProperty);
}

public static void SetTargetTextBlock(DependencyObject obj, TextBlock value)
{
    obj.SetValue(TargetTextBlockProperty, value); 
}

public static readonly DependencyProperty TargetTextBlockProperty =
    DependencyProperty.RegisterAttached("TargetTextBlock",
                                typeof(TextBlock),
                                typeof(RunExtender),
                                new PropertyMetadata(null, new PropertyChangedCallback(OnTargetAttached)));

And now it's time to do the actual parsing. What I want to do is take each run and make it either a new text block (we are mapping to multiple text blocks because the wrap panel is going to wrap on the outermost container ... if we put all the runs inside a single text block, it will only wrap in the text block and not in the context of the wrap panel container) or a hyperlink button. The hyperlink button will take on the attributes of the text block except the foreground (we'll keep the default of that being blue to show that it's a link). All of these will become children of the new wrap panel.

Don't forget that the wrap panel exists in the toolkit that you can download here. We simply need to add a reference to System.Windows.Controls.Toolkit in our project.

Here's the code:


public static void OnTargetAttached(object obj, DependencyPropertyChangedEventArgs args)
{
    WrapPanel panel = obj as WrapPanel;
    if (panel != null)
    {
        TextBlock src = args.NewValue as TextBlock;
        if (src != null)
        {
            foreach (Inline inline in src.Inlines)
            {
                if (inline is LineBreak)
                {
                    TextBlock newBlock = new TextBlock();
                    newBlock.Inlines.Add(new LineBreak());
                    panel.Children.Add(newBlock); 
                }
                else if (inline is Run)
                {
                    Run run = inline as Run;
                    Uri navigateUri = run.GetValue(NavigateUrlProperty) as Uri;
                    if (navigateUri != null)
                    {
                       HyperlinkButton button = new HyperlinkButton();
                        button.Content = run.Text;
                        button.NavigateUri = navigateUri;
                        _Clone(src, button, _tbInfo, _cInfo); 
                        panel.Children.Add(button);
                    }
                    else
                    {
                        Run newRun = new Run { Text = _reg.Replace(run.Text.Replace('\n',' ')," ").Trim() };
                        _Clone(run, newRun, _rInfo);
                        TextBlock newBlock = new TextBlock();
                        _Clone(src, newBlock, _tbInfo);
                        newBlock.Inlines.Add(newRun);
                        panel.Children.Add(newBlock);
                    }
                }
            }
        }
    }
}

Now that we've got that all in place, it's time to update our main page. First, we'll add a reference to the toolkit:


xmlns:controls="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Toolkit"

Next, we pull our text block into the resources section and give it a key. This makes it a resource now instead of an inline text block:


<UserControl.Resources>
        <TextBlock x:Key="tb" TextWrapping="Wrap" FontSize="20"><Run FontFamily="Times New Roman">Lorem ipsum dolor sit amet, consectetur adipiscing elit.</Run> 
            Aenean eget turpis id purus tempor tincidunt porttitor eu mi.<LineBreak/> 
            Here is an <Run local:RunExtender.NavigateUrl="http://www.wintellect.com/">inline hyperlink</Run>.
            <Run Foreground="Cyan">Vestibulum tincidunt odio quis nibh feugiat faucibus.</Run> 
            Fusce rhoncus tristique mi non posuere.<LineBreak/>
            Nunc sit amet velit magna.</TextBlock>
    </UserControl.Resources>

In our main grid, instead of the text block, we'll now use a wrap panel and set the text block as the target:


<Grid x:Name="LayoutRoot">
   <controls:WrapPanel local:RunExtender.TargetTextBlock="{StaticResource tb}"/>
</Grid>

Now we run it, and this is what we get:

Lorem Ipsum

It's the same text as before, but with a nice, clickable hyperlink. Clicking the hyperlink validates that it does indeed take us to the Wintellect home page.

Now you can add inline hyperlinks to your heart's desire simply by decorating the run and using the wrap panel.

There are several third party controls available that help with rendering HTML and hyperlinks that you can use, but I always believe it's helpful to understand the how and why. Hopefully this exercise assisted you with a better understanding of dependency objects and properties and more specifically the text rendering functionality that is available out of the box with Silverlight 3.

You may download this example here.

Jeremy Likness