views:

3648

answers:

9

I am having trouble getting Silverlight 2.0 to lay out text exactly how I want. I want text with line breaks and embedded links, with wrapping, like HTML text in a web page.

Here's the closest that I have come:

<UserControl x:Class="FlowPanelTest.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    xmlns:Controls="clr-namespace:Microsoft.Windows.Controls;assembly=Microsoft.Windows.Controls" 
    Width="250" Height="300">
    <Border BorderBrush="Black" BorderThickness="2" >
      <Controls:WrapPanel> 
      <TextBlock x:Name="tb1" TextWrapping="Wrap">Short text. </TextBlock>
      <TextBlock x:Name="tb2" TextWrapping="Wrap">A bit of text. </TextBlock>
      <TextBlock x:Name="tb3" TextWrapping="Wrap">About half of a line of text.</TextBlock>
      <TextBlock x:Name="tb4" TextWrapping="Wrap">More than half a line of longer text.</TextBlock>
      <TextBlock x:Name="tb5" TextWrapping="Wrap">More than one line of text, so it will wrap onto the  following line.</TextBlock>
      </Controls:WrapPanel>
      </Border>
</UserControl>

But the issue is that although the text blocks tb1 and tb2 will go onto the same line because there is room enough for them completely, tb3 onwards will not start on the same line as the previous block, even though it will wrap onto following lines.

I want each text block to start where the previous one ends, on the same line. I want to put click event handlers on some of the text. I also want paragraph breaks. Essentially I'm trying to work around the lack of FlowDocument and Hyperlink controls in Silverlight 2.0's subset of XAML.


To answer the questions posed in the answers:

Why not use runs for the non-clickable text? If I just use individual TextBlocks only on the clickable text, then those bits of text will still suffer from the wrapping problem illustrated above. And the TextBlock just before the link, and the TextBlock just after. Essentially all of it. It doesn't look like I have many opportunities for putting multiple runs in the same TextBlock.

Dividing the links from the other text with RegExs and loops is not the issue at all, the issue is display layout.

Why not put each word in an individual TextBlock in a WrapPanel Aside from being an ugly hack, this does not play at all well with linebreaks - the layout is incorrect.

It would also make the underline style of linked text into a broken line.

Here's an example with each word in its own TextBlock. Try running it, note that the linebreak isn't shown in the right place at all.

<UserControl x:Class="SilverlightApplication2.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
   xmlns:Controls="clr-namespace:Microsoft.Windows.Controls;assembly=Microsoft.Windows.Controls" 
    Width="300" Height="300">
    <Controls:WrapPanel>
        <TextBlock  TextWrapping="Wrap">Short1 </TextBlock>
        <TextBlock  TextWrapping="Wrap">Longer1 </TextBlock>
        <TextBlock  TextWrapping="Wrap">Longerest1 </TextBlock>
        <TextBlock  TextWrapping="Wrap">
                <Run>Break</Run>
                <LineBreak></LineBreak>
        </TextBlock>
        <TextBlock  TextWrapping="Wrap">Short2</TextBlock>
        <TextBlock  TextWrapping="Wrap">Longer2</TextBlock>
        <TextBlock  TextWrapping="Wrap">Longerest2</TextBlock>
        <TextBlock  TextWrapping="Wrap">Short3</TextBlock>
        <TextBlock  TextWrapping="Wrap">Longer3</TextBlock>
        <TextBlock  TextWrapping="Wrap">Longerest3</TextBlock>
    </Controls:WrapPanel>
</UserControl>

What about The LinkLabelControl as here and here. It has the same problems as the approach above, since it's much the same. Try running the sample, and make the link text longer and longer until it wraps. Note that the link starts on a new line, which it shouldn't. Make the link text even longer, so that the link text is longer than a line. Note that it doesn't wrap at all, it cuts off. This control doesn't handle line breaks and paragraph breaks either.

Why not put the text all in runs, detect clicks on the containing TextBlock and work out which run was clicked Runs do not have mouse events, but the containing TextBlock does. I can't find a way to check if the run is under the mouse (IsMouseOver is not present in SilverLight) or to find the bounding geometry of the run (no clip property).

There is VisualTreeHelper.FindElementsInHostCoordinates()

The code below uses VisualTreeHelper.FindElementsInHostCoordinates to get the controls under the click. The output lists the TextBlock but not the Run, since a Run is not a UiElement.

private void theText_MouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
    // get the elements under the click
    UIElement uiElementSender = sender as UIElement;
    Point clickPos = e.GetPosition(uiElementSender);
    var UiElementsUnderClick = VisualTreeHelper.FindElementsInHostCoordinates(clickPos, uiElementSender);

    // show the controls
    string outputText = "";
    foreach (var uiElement in UiElementsUnderClick)
    {
        outputText += uiElement.GetType().ToString() + "\n";
    }
    this.outText.Text = outputText;
}

Use an empty text block with a margin to space following content onto a following line

I'm still thinking about this one. How do you calculate the right width for a line-breaking block to force following content onto the following line? Too short and the following content will still be on the same line, at the right. Too long and the "linebreak" will be on the following line, with content after it. You would have to resize the breaks when the control is resized.

Some of the code for this is:

    TextBlock lineBreak = new TextBlock();
    lineBreak.TextWrapping = TextWrapping.Wrap;
    lineBreak.Text = " ";
    // need adaptive width 
    lineBreak.Margin = new Thickness(0, 0, 200, 0);
A: 

A very interesting question. You may need to create your own custom control to handle this type of layout. You could use Runs but overlay a transparent canvas on top of each run so that you could handle the click event tied to that run. Not an easy solution at all but I think it'd be possible.

Please let me know what you come with.

Tom
I've got a bad feeling about the phrase "interesting question".
Anthony
A: 

Why cant you use runs?

Use runs to concatonate all of the values that arent' going to have events, then those that have events break out into thier own text block, rinse lather repeat.

It seems to me that you should be able to do this with RegEx and some looping. Check out Jesse Liberty's post on wrap panel and see if that fosters any thought. http://silverlight.net/blogs/jesseliberty/archive/2008/12/03/the-wrap-panel.aspx

hth

I have addressed this in the question above.
Anthony
A: 

You could try using ONLY Runs inside your TextBlock, and use a single click handler for the whole TextBlock. The handler can then locate the source Run using the click event's coordinates, find out if it's a link (each Run which is a link could have a specific x:Name, or you could even derive your own Run) and invoke the right functionality for that link.

I have never tried it, but this is the way I would try to solve the problem.

JacobE
I am trying to do this, but how do you locate the run? You have the click event's co-ordinates, but I don't see how to find the run's bounds.
Anthony
A: 

I think the best way to do this would be to have each word be a textblock and then just attach the words to the correct event handlers you want for particular sections. This would give you the wrapping you want and for the indentation on the first word, you can set it's left margin.

See this article for an example in which something similar is done with text and the wrap panel. http://jesseliberty.com/2008/12/03/the-wrap-panel/

Tom
I have included an example of this technique going wrong.
Anthony
You can get around that problem by separating out your linebreaks into a textblock and setting either the left/right margin or the width of the linebreak textblocks to be the width of the wrappanel. forcing them to be on their own line. Not perfect but it might be the best you can do.
Tom
A: 

While there is no IsMouseOver type property, you could look at using VisualTreeHelper.FindElementsInHostCoordinates()

Tom
Nice idea, I didn;t know about that function. I have discussed it above.
Anthony
A: 

I'm not sure if you're checking the comments on your comments or not because you didn't address a few things I said in my comments back on yours. The reason you can't find runs using VisualTreeHelper.FindElementsInHostCoordinates() is because it only returns UIElements and Runs aren't UIElements. If you combine this method though with the suggestion not to use any runs then you should be fine right?

The, every word as a textblock isn't really that bad of a "hack" and you can get around the issue of linebreaks by separating the linebreaks out into their own textblock and assigning those textblocks a width or margin equal to the width of the wrap panel which would force them to sit on their own line. Definitely not the most ideal solution but I haven't see anything else that shows any promise yet.

Tom
I haven't had time to try this - comments above.
Anthony
A: 

Would this work? http://www.silverlightshow.net/items/Silverlight-LinkLabel-control.aspx

No. That's just another case of putting each word in it's own text block. It has severe problems with line and paragraph breaks, as noted in the question.
Anthony
Also, chnge the link text in that sample to "links with long text are not wrapped at all by this control". The link now starts on a new line when it shouldn't. WrapPanel doesn't have a way around this short of breaking the link up into separate words. Which would be bad if the link was underlined.
Anthony
Or change the link to "[link="http://www.silverlightshow.net" target="_blank"]the quick brown fox jumped over the laxy dog the quick brown dog jumped over the lazy dog[/link]" - the linkbutton can't wrap at all.
Anthony
A: 

Try this: http://blogs.msdn.com/delay/archive/2007/09/10/bringing-a-bit-of-html-to-silverlight-htmltextblock-makes-rich-text-display-easy.aspx

The HtmlTextBlock for Silverlight. It's not really a finsihed concept, but it might be a good starting point.

it's a good control, and if the links were clickable then it would be just what I wanted. But it puts all the text in runs. And they aren't clickable. See comments in the question.
Anthony
to make it clear: this is what I wanted, but unless the links are clickable, it's not so much a "starting point" as a dead end with a chain-link fence through which I can see the destination.
Anthony
+2  A: 

I'm going to put in some answers to my own question, based on what I have found:

1) you can do this easily in the full-fat desktop WPF with a flow document full of paragraph, hyperlink, run and related objects.

This is what I am doing now, I am not trying to solve this problem in Silverlight any more.

2) Use Silverlight 4 You can't do this in Silverlight 2 or 3. However Silverlight 4 has a RichTextArea Control which, when readonly, supports this kind of display of flow layout with inline hyperlinks, and so acts like a cut-down version of FlowDocument and related classes from WPF. Silverlight 4 also allows Embedded Web browser control to display HTML content, if you can make it look that same across Windows versions (i.e. IE versions) and on Mac and potentially other platforms.

3) You can probably do something like this in Silverlight (any version) by building up a string of HTML and injecting it into the DOM to show it in the part of the page that's outside the Silverlight control. It sounds entirely workable, but in my opinion, too clever by half.

Anthony