Configuring the WP7 Clipboard with C1RichTextBox

On the Windows Phone you get automatic copy and paste support for standard TextBoxes. For other controls you wish to enable copy/paste functionality you typically have to use a TextBox in disguise. To put text to the clipboard you simply call the static SetText method on the System.Windows.Clipboard class like this:


Clipboard.SetText("some text");  

The ComponentOne RichTextBox control is part TextBox and part custom control. As of the 2012 v1 version the control does not have built-in copying functionality, but if text resides on the clipboard then you can paste it into C1RichTextBox for free. So in order to enable copying text we must add it ourselves. To put plain text on the clipboard the code would simply be:


// set clipboard text to plain text  
Clipboard.SetText(c1RichTextBox1.SelectedText);  

Next, we can create a custom round button that displays when the user selects some text in C1RichTextBox just as it would for a standard TextBox. To create a round button we override the style to give it rounded corners (Border CornerRadius = 33).


<Style x:Key="RoundButtonStyle" TargetType="Button">  
    <Setter Property="Background" Value="{StaticResource PhoneBackgroundBrush}"/>  
    <Setter Property="BorderBrush" Value="{StaticResource PhoneForegroundBrush}"/>  
    <Setter Property="Foreground" Value="{StaticResource PhoneForegroundBrush}"/>  
    <Setter Property="BorderThickness" Value="{StaticResource PhoneBorderThickness}"/>  
    <Setter Property="FontFamily" Value="{StaticResource PhoneFontFamilySemiBold}"/>  
    <Setter Property="FontSize" Value="{StaticResource PhoneFontSizeMediumLarge}"/>  
    <Setter Property="Padding" Value="0"/>  
    <Setter Property="Template">  
        <Setter.Value>  
            <ControlTemplate TargetType="Button">  
                <Grid Background="Transparent">  
                    <VisualStateManager.VisualStateGroups>  
                        <VisualStateGroup x:Name="CommonStates">  
                            <VisualState x:Name="Normal"/>  
                            <VisualState x:Name="MouseOver"/>  
                            <VisualState x:Name="Pressed">  
                                <Storyboard>  
                                    <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Foreground" Storyboard.TargetName="ContentContainer">  
                                        <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneBackgroundBrush}"/>  
                                    </ObjectAnimationUsingKeyFrames>  
                                    <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Background" Storyboard.TargetName="ButtonBackground">  
                                        <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneForegroundBrush}"/>  
                                    </ObjectAnimationUsingKeyFrames>  
                                    <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="BorderBrush" Storyboard.TargetName="ButtonBackground">  
                                        <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneForegroundBrush}"/>  
                                    </ObjectAnimationUsingKeyFrames>  
                                </Storyboard>  
                            </VisualState>  
                            <VisualState x:Name="Disabled">  
                                <Storyboard>  
                                    <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Foreground" Storyboard.TargetName="ContentContainer">  
                                        <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneDisabledBrush}"/>  
                                    </ObjectAnimationUsingKeyFrames>  
                                    <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="BorderBrush" Storyboard.TargetName="ButtonBackground">  
                                        <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneDisabledBrush}"/>  
                                    </ObjectAnimationUsingKeyFrames>  
                                    <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Background" Storyboard.TargetName="ButtonBackground">  
                                        <DiscreteObjectKeyFrame KeyTime="0" Value="Transparent"/>  
                                    </ObjectAnimationUsingKeyFrames>  
                                </Storyboard>  
                            </VisualState>  
                        </VisualStateGroup>  
                    </VisualStateManager.VisualStateGroups>  
                    <Border x:Name="ButtonBackground" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" CornerRadius="33" Margin="{StaticResource PhoneTouchTargetOverhang}">  
                        <ContentControl x:Name="ContentContainer" ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" Foreground="{TemplateBinding Foreground}" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" Padding="{TemplateBinding Padding}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}">  
                            <ContentControl.RenderTransform>  
                                <ScaleTransform x:Name="buttonScale" />  
                            </ContentControl.RenderTransform>  
                        </ContentControl>  
                    </Border>  
                </Grid>  
            </ControlTemplate>  
        </Setter.Value>  
    </Setter>  
</Style>  

Then we can add a button to our page containing C1RichTextBox like this:


<Button x:Name="btnCopyPlainText"  
        Width="72"  
        Height="72"  
        Click="btnCopyPlainText_Click"  
        Margin="162,160, 0, 0"  
        HorizontalAlignment="Left"  
        VerticalAlignment="Top"  
        Style="{StaticResource RoundButtonStyle}"  
        Visibility="Collapsed" >  
    <Image x:Name="imgPlainText"  
           Source="Resources\\Copy.png"  
           Stretch="Fill" />  
</Button>  

Notice that we are setting the HorizontalAlignment, VerticalAlignment and Margin. At run-time we will update the Margin to reflect the position of the selected text. To detect the selection changing we simply subscribe to the C1RichTextBox SelectionChanged event.


private void c1RichTextBox1_SelectionChanged(object sender, EventArgs e)  
{  
    PositionCopyButton();  
}  

We can grab the Rect of the selected text by using the GetRectFromPosition method and passing in the SelectedText Start and End positions. From this Rect we set the Left and Top margin values of our button so that it appears to float above the selected text.


private void PositionCopyButton()  
{  
    if (!string.IsNullOrEmpty(c1RichTextBox1.SelectedText))  
    {  
        // get bounds of selected text  
        Rect rtStart = c1RichTextBox1.GetRectFromPosition(c1RichTextBox1.Selection.Start);  
        Rect rtEnd = c1RichTextBox1.GetRectFromPosition(c1RichTextBox1.Selection.End);  

        // center button above selected text  
        btnCopyPlainText.Margin = new Thickness(rtStart.X + ((rtEnd.X - rtStart.X) / 2) - btnCopyPlainText.Width / 2, rtStart.Y - btnCopyPlainText.Height, 0, 0);  
        btnCopyPlainText.Visibility = System.Windows.Visibility.Visible;  
    }  
    else  
    {  
        // hide button if no text to copy  
        btnCopyPlainText.Visibility = System.Windows.Visibility.Collapsed;  
    }  
}  

Then after we set the selected text to the clipboard we must set the Focus from the copy button back to C1RichTextBox. We can do this by simply calling the Focus method.


private void btnCopyPlainText_Click(object sender, RoutedEventArgs e)  
{  
    // set clipboard text to plain text  
    Clipboard.SetText(c1RichTextBox1.SelectedText);  

    // keep focus on richtextbox  
    c1RichTextBox1.Focus();  
}  

That is it for copying plain text!

Copying Rich Text to the Clipboard

We can easily obtain the rich (html) text from the C1RichTextBox selection using the Html property. We can also easily set this to the clipboard.


private void btnCopyRichText_Click(object sender, RoutedEventArgs e)  
{  
    // set clipboard text to html text  
    Clipboard.SetText(c1RichTextBox1.Selection.Html);  

    // keep focus on richtextbox  
    c1RichTextBox1.Focus();  
}  

The problem is that when we paste this we are pasting the raw Html as plain text. There’s no way just simply tell the control to PASTE rich text as well. It's possible to fix this. When the C1RichTextBox TextChanged event is fired, we are given the complete C1TextRange object that contains all of the inserted html text from a Paste action. So quite simply we can replace the range’s underlying HTML with the plain text that contains the desired Html. Sound confusing? This line of code does what I’m trying to explain.


// sets rich html text from clipboard into the text range  
range.Html = range.Text;  

At the time of pasting, the Text property contains the Selection.Html content we just set to the clipboard. That one line of code is the key but there's a bit more required to actually get it to work. One small caveat to setting the text of C1RichTextBox and that is that you should not do it directly within the TextChanged event. I’ve found that it usually crashes so I’ve found more luck in using a timer or storyboard to delay the action. There are also a few clean-up steps required, like setting the caret to the end of the selection after pasting is completed, and removing a Paragraph tag that always gets added to Selection.Html. Here is the complete code to handle special pasting of html.


bool _copyRichText;  
bool _updating;  
Storyboard _storyBoard;  
C1TextRange _range;  

private void c1RichTextBox1_TextChanged(object sender, C1TextChangedEventArgs e)  
{  
    // handle pasting html  
    if (e.Range.Text.Contains("<html>") && _copyRichText)  
    {  
        // start timer whenever document receives html text from clipboard  
        // we can't directly change text within this event so we do it in storyboard_Completed event  
        if (!_updating)  
        {  
            _range = e.Range;  
            if (_storyBoard == null)  
            {  
                _storyBoard = new Storyboard();  
                \_storyBoard.Completed += new EventHandler(storyboard\_Completed);  
                _storyBoard.Duration = new Duration(TimeSpan.FromMilliseconds(1));  
            }  
            _storyBoard.Stop();  
            _storyBoard.Seek(TimeSpan.Zero);  
            _storyBoard.Begin();  
        }  
    }  
}  

void storyboard_Completed(object sender, EventArgs e)  
{  
    _updating = true;  
    UpdatePastedText(_range);  
    _updating = false;  
}  

private void UpdatePastedText(C1TextRange range)  
{  
    // remove unnecessary paragraph tag  
    range.Text = range.Text.Remove(range.Text.LastIndexOf("</p>"), 4);  
    int p = range.Text.LastIndexOf("<p");  
    string pTag = range.Text.Substring(p, 20); // assumes > tag is within 20 characters of <p  
    range.Text = range.Text.Remove(p, pTag.IndexOf(">") + 1);  

    // sets rich html text from clipboard into the text range  
    range.Html = range.Text;  

    // put caret at the end of pasted selection  
    c1RichTextBox1.Select(range.End.TextOffset, 0);  
}  

You can download the sample below. Download Sample

Greg Lutz

comments powered by Disqus