This is a migrated thread and some comments may be shown as answers.

RichTextBox with multiple DataProviders

8 Answers 281 Views
RichTextBox
This is a migrated thread and some comments may be shown as answers.
DKO
Top achievements
Rank 1
DKO asked on 17 May 2018, 10:58 AM

Hello,

Let's start by explaining what I'm trying to achieve. I basically want to bind a single RadRichTextBox to multiple different DataProviders.
In my specific situation one RtfDataProvider and one TxtDataProvider. So that the same text can be bound to two different properties.
Where one property is just plain text and the other has the applied format (rtf).

I just went ahead and just tried it, and to my surprise this actually worked perfectly fine. However when I tried to apply the same technique to a custom RadGridView.RowDetailsTemplate this quit working and only the format of the last defined DataProvider gets applied.

One could think that this isn't really that big of an issue if you just define the RtfDataProver last, because then the RadRichTextBox would always have the correct styles applied.
This train of though would be correct when you can assume that both properties will always contain the same values, which unfortunately is not the case in my specific scenario.

Here is what happened, my model (and corresponding database table) used to only have the property for plain text.
We've recently decided to add an extra property (or column in the database) to save text formatting (rtf).
The great thing about binding both of the properties to the different DataProviders was that even if the rtf-property was empty/null then the RadRichTextBox would still show the plain text and apply default formatting to it.

When using this technique in the RowDetailsTemplate then the following situations occurred:

  • RtfDataProvider is defined last: when the rtf-property is empty the RadRichTextBox will always be empty even when the plain text-property has a value.
  • TxtDataProvider is defined last: the RadRichTextBox will always show some text but won't apply any formatting to it even when there is some defined in the rtf-property.

To explain my scenario better I'll add some code to helpfully make things easier to understand. (This isn't my actual code, just a simpler version to get help sketch an idea)

1. My basic model that represents the table in our database

public class MyTaskObject : INotifyPropertyChanged
{
    private long _taskId;
    public long TaskId
    {
        get { return _taskId; }
        set { SetProperty(ref _taskId, value); }
    }
     
    private string _title;
    public string Title
    {
        get { return _title; }
        set { SetProperty(ref _title, value); }
    }
 
    private string _text;
    public string Text
    {
        get { return _text; }
        set { SetProperty(ref _text, value); }
    }
 
    private string _rtfText;
    public string RtfText
    {
        get { return _rtfText; }
        set { SetProperty(ref _rtfText, value); }
    }
 
    #region Observable
    public event PropertyChangedEventHandler PropertyChanged;
 
    protected void OnPropertyChanged([CallerMemberName] string propName = null)
    {
        var handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(propName));
        }
    }
 
    protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
    {
        if (Equals(storage, value)) return false;
        storage = value;
        OnPropertyChanged(propertyName);
        return true;
    }
    #endregion
   
}

 

2. The first scenario where binding the 2 properties to different DataProviders and a single RadRichTextBox works perfectly as desired.

2.1 This is the Xaml of the view: it's a simple user control that allows the user to search a the database for a specific "task", load it and edit before saving it.
So imagine there to be a buttons to look for "tasks"  and commit or discard the changes.
There are also some textboxes to edit the edit the "Title" property for example, followed by the following:

<telerik:RtfDataProvider x:Name="TaskRtfProvider" Grid.Row="1" Grid.Column="0"  RichTextBox="{Binding ElementName=TaskEditor}"  SetupDocument="DataProviderBase_OnSetupDocument" Rtf="{Binding SelectedTask.RtfText, Mode=TwoWay, UpdateSourceTrigger=Explicit}" />
<telerik:TxtDataProvider x:Name="TaskTxtProvider" Grid.Row="1" Grid.Column="0"  RichTextBox="{Binding ElementName=TaskEditor}" SetupDocument="DataProviderBase_OnSetupDocument"  Text="{Binding SelectedTask.Text, Mode=TwoWay, UpdateSourceTrigger=Explicit}" />
<telerik:RadRichTextBox Grid.Row="1" Grid.Column="0" x:Name="TaskEditor"  Margin="5,0" IsImageMiniToolBarEnabled="True" IsSpellCheckingEnabled="False" Padding="4,2"  FontSize="11pt" FontFamily="Calibri" LayoutMode="Flow"  DocumentInheritsDefaultStyleSettings="True"/>

The UpdateSourceTrigger is Explicit because I use changed tracking and only wish to update the database when the user explicitly says to be clicking a button.
Because if I don't do it this way, and the user loads a task where the "RtfText" property is empty, then immediately after loading this will have a value defined by the default formatting of the RadRichTextBox.

In this first scenario these bindings actually work as they're supposed to.

2.2 The implementation of the SetupDocument-event, this is used to apply some basic formatting (mainly to remove excessive spacing between lines)

private void DataProviderBase_OnSetupDocument(object sender, SetupDocumentEventArgs e)
{
    //var test = sender;
    if (e.Document == null) return;
    e.Document.ParagraphDefaultSpacingAfter = 0;
}

While debugging I've noticed that the second defined DataProvider (in this example the "TaskTxtProvider") does always trigger this event, only when the first DataProvider is null does this event get triggered twice.
Which is exactly what makes this scenario work because this way the RadRichTextBox shows the formatted text if there is any, and else applies the default formatting to the plain text.
This is of course the reason for defining the RtfDataProvider ("TaskRtfProvider")  first.

3. The second scenario is where this stops working, and where I tried to place a RadRichTextBox into the rows of a RadGridView.
In this scenario the RadRichTextBox is purely used for displaying the data, not editing.

                           

8 Answers, 1 is accepted

Sort by
0
DKO
Top achievements
Rank 1
answered on 17 May 2018, 11:38 AM

I accidentally posted while still editing and there is no way of editing my previous post, so here is the rest of it:

3.1 The Xaml of the view is a simple user control to show all the "tasks" in the database.

<telerik:RadGridView x:Name="MyTaskGrid" Grid.Row="2" Margin="5,2"
    AutoGenerateColumns="False"
    SelectionMode="Single"
    SelectionUnit="FullRow"
    IsReadOnly="True"
    ShowGroupPanel="False"
    IsFilteringAllowed="False"
    ItemsSource="{Binding TaskList}"
    SelectedItem="{Binding SelectedTask}"
    FilteringMode="Popup"
    CanUserFreezeColumns="False"
    ValidatesOnDataErrors="None"
    CanUserReorderColumns="False"
    GridLinesVisibility="Vertical"
    RowDetailsVisibilityMode="Visible"
    RowIndicatorVisibility="Collapsed"
    ScrollViewer.HorizontalScrollBarVisibility="Disabled">
 
<telerik:RadGridView.Columns>
    <telerik:GridViewDataColumn  Header="TaskId" DataMemberBinding="{Binding TaskId, Mode=OneWay}" Width="100" />
    <telerik:GridViewDataColumn  Header="Title" DataMemberBinding="{Binding Title, Mode=OneWay}" Width="200" />
</telerik:RadGridView.Columns>
 
<telerik:RadGridView.RowDetailsTemplate>
    <DataTemplate>
        <Border BorderThickness="0,0,0,1" BorderBrush="Black">
            <Grid>
                <telerik:RtfDataProvider RichTextBox="{Binding ElementName=MyTaskEditor}"  SetupDocument="DataProviderBase_OnSetupDocument" Rtf="{Binding RtfText, Mode=OneWay}" />
                <telerik:TxtDataProvider RichTextBox="{Binding ElementName=MyTaskEditor}" SetupDocument="DataProviderBase_OnSetupDocument"  Text="{Binding Text, Mode=OneWay}" />
                <telerik:RadRichTextBox  Margin="5,2,5,5" MaxHeight="200"  x:Name="MyTaskEditor" IsContextMenuEnabled="False"  IsReadOnly="True"  IsImageMiniToolBarEnabled="False" IsSelectionMiniToolBarEnabled="False"  IsSpellCheckingEnabled="False" Padding="4,2"  FontSize="11pt" FontFamily="Calibri" LayoutMode="Flow"  DocumentInheritsDefaultStyleSettings="True"/>
            </Grid>
        </Border>
    </DataTemplate>
</telerik:RadGridView.RowDetailsTemplate>
</telerik:RadGridView>

The RowDetails now contains the RadRichTextBox with OneWay bindings this time because there won't be any editing anyway.
The same SetupDocument-event implementation as before was used to apply default formatting, but in this scenario the event will always be triggered twice for each row.
Regardless if the "RtfText" property is null or not (In the previous scenario 2.2 this would only be triggered if it is null).
So in this example there will never be any additional formatting applied because the TxtDataProvider was defined last.
And if I were to put the TxtDataProvider last, then there would be additional formatting but in some cases no text would be shown even if there is plain text.

 

I hope I was able to explain the situation clearly, and appreciate any suggestions on getting this to work properly.

Or could someone explain to me the life-cyle of a DataProvider?
Because I've tested with placing the RadRichTextBox of the first example in a new window and showing it as dialogue, and the SetupDocument-event some how still triggered some times even after the window was closed.
Which makes me believe that there might be an issue with the disposal of the DataProviders.

 

0
Boby
Telerik team
answered on 22 May 2018, 01:25 PM
Hi DKO,

I tried to reproduce the issue, but in my tests the behavior was pretty consistent - the second data provider always wins and has its document loaded (the project is attached). 

Anyway, I just checked with the team and it seems the usage scenario is somewhat unusual and not something tested and expected to behave in any particular way. My suggestion would be to move the logic to the view model instead - there you can expose a single RtfText property, and compute its value based on the model's RtfText and Text properties, e.g. if the both are full, return the value of  model's RtfText, if only model's Text is full, convert the document to RTF using the RtfFormatProvider and TxtFormatProvider and return the converted value.

Would this suggestion work for you?

Regards,
Boby
Progress Telerik
Want to extend the target reach of your WPF applications, leveraging iOS, Android, and UWP? Try UI for Xamarin, a suite of polished and feature-rich components for the Xamarin framework, which allow you to write beautiful native mobile apps using a single shared C# codebase.
0
DKO
Top achievements
Rank 1
answered on 25 May 2018, 02:52 PM

I guess your suggestion works but could you give me an example of converting the plain text to RTF?

0
Boby
Telerik team
answered on 28 May 2018, 10:21 AM
Hi DKO,

Yes, sure, the code is as follows:
string text = "...";
 
var radDocument = new TxtFormatProvider().Import(text);
string rtf = new RtfFormatProvider().Export(radDocument);


Regards,
Boby
Progress Telerik
Want to extend the target reach of your WPF applications, leveraging iOS, Android, and UWP? Try UI for Xamarin, a suite of polished and feature-rich components for the Xamarin framework, which allow you to write beautiful native mobile apps using a single shared C# codebase.
0
DKO
Top achievements
Rank 1
answered on 29 May 2018, 08:57 AM

The providers aren't disposable, are you certain the garbage collector will clean them correctly?

I'm kind of worried about possible memory leaks, because I've noticed some strange interactions when using a DataProvider in a dialogue window.
Even after the window was closed, if I changed the two-way bound property the SetupDocument event would sometimes still get triggered.

I've also been using the DataProviders, in your example you use FormatProviders, what is the difference?

 

0
Boby
Telerik team
answered on 30 May 2018, 06:21 AM
Hi DKO,

Data providers are relatively simple XAML-friendly wrappers of the format providers. They are created with the idea to be easy to use them in the view for data binding.

Format providers itself doesn't hold any unmanaged resources, so they don't need to implement IDisposable, and the garbage collector will free the memory used by them when they are no longer in use. If the creation of the format providers is in hot path (which I believe is not your case, as they would be probably instantiated once per dialog showing) you can reuse one and the same instance for multiple conversions:
var txtFormatProvider = new TxtFormatProvider();
var doc1 = txtFormatProvider.Import(text1);
var doc2 = txtFormatProvider.Import(text2);

Another approach would be to create a simple "migration" tool, which process the whole database at once, updating the plain text field to RTF, again using the format providers.

Regards,
Boby
Progress Telerik
Want to extend the target reach of your WPF applications, leveraging iOS, Android, and UWP? Try UI for Xamarin, a suite of polished and feature-rich components for the Xamarin framework, which allow you to write beautiful native mobile apps using a single shared C# codebase.
0
DKO
Top achievements
Rank 1
answered on 30 May 2018, 09:11 AM

I was just worried for the second scenario, where I wanted to use the RichTextbox as a rowdetail of a datagrid.

So if I were to implement your suggestion I would do the following changes to my model:

public class MyTaskObject : INotifyPropertyChanged
{
    //...
    private string _text;
    public string Text
    {
        get { return _text; }
        set { SetProperty(ref _text, value); }
    }
 
    private string _rtfText;
    public string RtfText
    {
        get
        {
            if(string.IsNullOrWhiteSpace(_rtfText) &&  !string.IsNullOrWhiteSpace(_text))
            {
                var doc =  new  TxtFormatProvider().Import(_text);
                return  new  RtfFormatProvider().Export(doc);
            
            return  _rtfText;
        }
        set { SetProperty(ref _rtfText, value); }
    }
     //...
}

 

And then simply place the RtfDataProvider as last on my template.

Or would I be better to create some sort of MultiValueConverter where I return the first value if it is different to null, else use the FormatProviders to convert the second value to Rtf?

0
Boby
Telerik team
answered on 31 May 2018, 06:48 AM
Hello DKO,

Yes, a model implementation like this is exactly what I was thinking about. 

About the multi-value converter suggestion: According to me, it's a matter of personal preference. One of the main goals of MVVM is to ensure that the code would be testable, which is achieved with both approaches (you can write unit tests against the model and against the multi-value converter).

I think I would go with the "model" approach, as it is simpler, without requiring additional knowledge about relatively advanced WPF features like IMultiValueConverter, but I may be missing some of your context here. For example, if the conversion should be used in multiple unrelated models, it may be suitable to package the functionality in a converter for easier reuse.

Regards,
Boby
Progress Telerik
Want to extend the target reach of your WPF applications, leveraging iOS, Android, and UWP? Try UI for Xamarin, a suite of polished and feature-rich components for the Xamarin framework, which allow you to write beautiful native mobile apps using a single shared C# codebase.
Tags
RichTextBox
Asked by
DKO
Top achievements
Rank 1
Answers by
DKO
Top achievements
Rank 1
Boby
Telerik team
Share this question
or