2

I have a problem with the performance of the wpf gui.

At first I will explain what I have done. I read from a Database different chat data, mostly text but sometimes there is an icon in the middle of the text, like a smiley or similar. Or, there are no text just a Image.

I have this all done by using a Flowdocument and use a Textblock with inlines. Oh I forgot, I use wpf, sorry.

Thats work great, BUT at the moment the Flowdocument will be painted to the RichTextbox or FlowdocumentReader, its take a long time and the gui freeze. I have think about Virtualizing but a RichTextBox doesn't use this. So my next idea was to use a Listbox and set as item a Richtextbox for every Chatbubble. A Chat can contain round about 20.000 Chatbubbles. So now I want to use Databinding but I doesn't find a way to bind the inlines of a Textblock.

So now some code.

<DataTemplate x:Key="MessageDataTemplate" DataType="{x:Type classes:Message}">
            <Grid>
                <RichTextBox x:Name="rtbChat"
                    SpellCheck.IsEnabled="False"
                    VerticalScrollBarVisibility="Auto"
                    VerticalContentAlignment="Stretch">
                    <FlowDocument
                        FontFamily="Century Gothic"
                        FontSize="12"
                        FontStretch="UltraExpanded">
                        <Paragraph>
                            <Figure>
                                <BlockUIContainer>
                                    <Border>
                                        <Border>
                                            <Grid>
                                                <Grid.ColumnDefinitions>
                                                    <ColumnDefinition Width="150"/>
                                                    <ColumnDefinition Width="80"/>
                                                </Grid.ColumnDefinitions>
                                                <Grid.RowDefinitions>
                                                    <RowDefinition Height="15"/>
                                                    <RowDefinition Height="Auto"/>
                                                </Grid.RowDefinitions>
                                                <TextBlock x:Name="tUser"
                                                    Foreground="Gray"
                                                    TextAlignment="Right"
                                                    FontSize="10"
                                                    Grid.Row="0"
                                                    Grid.Column="1"
                                                    Text="{Binding displayUserName}"/>

                                                <TextBlock x:Name="tTime"
                                                    Foreground="Gray"
                                                    TextAlignment="Left"
                                                    FontSize="10"
                                                    Grid.Row="0"
                                                    Grid.Column="0"
                                                    Text="{Binding sendTime}"/>

                                                <TextBlock x:Name="tMessage"
                                                    Foreground="Black"
                                                    TextAlignment="Justify"
                                                    FontSize="12"
                                                    Height="NaN"
                                                    TextWrapping="Wrap"
                                                    Grid.Row="1"
                                                    Grid.Column="0"
                                                    Grid.ColumnSpan="2"
                                                    Text="{Binding contentText}"   />
                                                <Image x:Name="tImage"
                                                    Grid.Row="1"
                                                    Grid.Column="0"
                                                    Grid.ColumnSpan="2"
                                                    Height="NaN"
                                                    Source="{Binding imageSend}"/>
                                            </Grid>
                                        </Border>
                                    </Border>
                                </BlockUIContainer>
                            </Figure>
                        </Paragraph>
                    </FlowDocument>
                </RichTextBox>
            </Grid>
        </DataTemplate>

So this is not final, I'm porting this from Source-code to xaml and some setters are missing at this moment.

I have benchmark the timings and everything works fine, 10 ms for the sqlite, round about 4 sec for the building of the FlowDocument but up to 5 min to paint the FlowDocument in the RichTextBox. I know that is why the hole box is painted, also the part that is not visible.

I hope that is understandable, if not ask me :)

Here the Source-Code before ported to xaml.

        var rtBox = new RichTextBox
        {
            //IsEnabled = false,
            BorderThickness = new Thickness(0, 0, 0, 0)
        };
        var doc = new FlowDocument();

        Contact contact = null;
        contact = _mess.remote_resource != "" ? _contacts.Find(x => x._jid == _mess.remote_resource) : _contacts.Find(x => x._jid == _mess.key_remote_jid);

        var para = new Paragraph();

        //--- Style of the message -----
        para.Padding = new Thickness(0);

        BlockUIContainer blockUI = new BlockUIContainer();
        blockUI.Margin = new Thickness(0, 0, 0, 0);
        blockUI.Padding = new Thickness(0);
        blockUI.TextAlignment = _mess.key_from_me == 1 ? TextAlignment.Right : TextAlignment.Left;

        Border bShadow = new Border();
        bShadow.Width = 231;
        bShadow.BorderBrush = Brushes.LightGray;
        bShadow.BorderThickness = new Thickness(0, 0, 0, 1);

        Border b2 = new Border();
        b2.Width = 230;
        b2.BorderBrush = Brushes.Gray;
        b2.Background = Brushes.White;
        b2.BorderThickness = new Thickness(0.5);
        b2.Padding = new Thickness(2);

        Grid g = new Grid();
        g.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(150,GridUnitType.Star) });
        g.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(80) });
        g.RowDefinitions.Add(new RowDefinition() { Height = new GridLength(15) });
        g.RowDefinitions.Add(new RowDefinition() { Height = new GridLength(25,GridUnitType.Auto) });

        TextBlock tUser = new TextBlock()
        {
            Foreground = Brushes.Gray,
            TextAlignment = TextAlignment.Right,
            FontSize = 10,
        };
        tUser.SetValue(Grid.RowProperty, 0);
        tUser.SetValue(Grid.ColumnProperty, 1);
        if(contact != null)
            tUser.Text = _mess.key_from_me == 1 ? "ich" : (contact._displayName == "" ? Whatsapp.Contacs.convertJidToNumber(_mess.remote_resource) : contact._displayName);
        else
        {
            tUser.Text = Whatsapp.Contacs.convertJidToNumber(_mess.remote_resource);
        }

        TextBlock tTime = new TextBlock()
        {
            Foreground = Brushes.Gray,
            TextAlignment = TextAlignment.Left,
            FontSize = 10,
        };
        tTime.SetValue(Grid.RowProperty, 0);
        tTime.SetValue(Grid.ColumnProperty, 0);
        tTime.Text = UnixTime.TimeReturnUnix2DateUtc(_mess.timestamp, timeZone).ToString();

        TextBlock tMessage = new TextBlock()
        {
            Foreground = Brushes.Black,
            TextAlignment = TextAlignment.Justify,
            FontSize = 12,
            Height = Double.NaN,
            TextWrapping = TextWrapping.Wrap

        };

        tMessage.SetValue(Grid.RowProperty, 1);
        tMessage.SetValue(Grid.ColumnProperty, 0);
        tMessage.SetValue(Grid.ColumnSpanProperty, 2);



        for (var i = 0; i < _mess.data.Length; i += Char.IsSurrogatePair(_mess.data, i) ? 2 : 1)
        {
            var x = Char.ConvertToUtf32(_mess.data, i);

            if (EmojiConverter.EmojiDictionary.ContainsKey(x))
            {

                //Generate new Image from Emoji
                var emoticonImage = new Image
                {
                    Width = 20,
                    Height = 20,
                    Margin = new Thickness(0, -5, 0, -5),
                    Source = EmojiConverter.EmojiDictionary[x]
                };

                //add grafik to FlowDocument
                tMessage.Inlines.Add(emoticonImage);
            }
            else
            {
                tMessage.Inlines.Add(new Run("" + _mess.data[i]));
            }
        }

        g.Children.Add(tUser);
        g.Children.Add(tTime);
        g.Children.Add(tMessage);

        b2.Child = g;
        bShadow.Child = b2;

        blockUI.Child = bShadow;

        Figure fig = new Figure(blockUI);
        fig.Padding = new Thickness(0);
        fig.Margin = new Thickness(0);
        fig.Height = new FigureLength(0, FigureUnitType.Auto);

        para.Inlines.Add(fig);

        doc.Blocks.Add(para);
        rtBox.Document = doc;
        msgList.Add(rtBox);

Greetings and thanks for your help.

nefas
  • 23
  • 3
  • 2
    Post a smaller but complete code sample so people can copy/paste it at their desk and help you then. – aybe Sep 11 '14 at 15:16
  • So you'll potentially have 20,000 `RichTextBox` in memory? That doesn't look like a good idea. – Patrice Gahide Sep 11 '14 at 15:28
  • That's nothing, a piece of software a "friend" wrote at this company "they" worked for made a scatter chart using a table with 40,000 cells in it. Twice. In one xaml. And for each modal dialogue box had fresh xml for that too that's 4000+ lines of xml. And it worked. So I wouldn't worry too much :) [Actually, no, the opposite of that, worry a lot.] – Mardoxx Sep 11 '14 at 15:42
  • So i know that is not the best solution :) I'm open for every other idea. My problem is that i will display text like that: bla bla (smiley as png) bla bla (smiley smiley) bla – nefas Sep 11 '14 at 15:53
  • Have you thought of converting the contents of each message to a visual? – jfin3204 Sep 11 '14 at 17:20

1 Answers1

1

One method would be to virtualize using a ListBox, certainly. Arguably better methods would be to dynamically load in the required messages or make your own virtualized control (issues with the default ListBox virtualization include that you have to scroll entire items in a single go to get virtualization working... which can suck a bit from a UX perspective in some cases.)

From the sound of it still taking forever to load, the virtualization you've set up isn't working right...

The main thing that you require to get virtualization working is that you need to have the ScrollViewer inside the ListBox template have CanContentScroll=True. Ie do:

<ListBox ScrollViewer.CanContentScroll="True" .... >

Or give the ListBox a template similar to below:

<ControlTemplate>
    <Border BorderBrush="{TemplateBinding Border.BorderBrush}"
            BorderThickness="{TemplateBinding Border.BorderThickness}"
            Background="{TemplateBinding Panel.Background}"
            SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}">
        <ScrollViewer Focusable="False"
                      Padding="{TemplateBinding Control.Padding}"
                      MaxHeight="{TemplateBinding Control.MaxHeight}"
                      CanContentScroll="True">
            <ItemsPresenter SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}" />
        </ScrollViewer>
    </Border>
</ControlTemplate>

Also, unless you want to actually select previous messages, maybe a ListBox isn't what you want, and you actually want an ItemsControl? See Virtualizing an ItemsControl? for more on that.

Addition 1 - Smooth Scrolling + Virtualization:

See below - if you also want smooth scrolling, might be worth looking at a TreeView - see http://classpattern.com/smooth-scrolling-with-virtualization-wpf-list.html#.VBHWtfldXSg - though I can't vouch for if this actually works at the moment, just discovered it myself!

Addition 2 - Clarification RE needed elements

As in my comments below, if you're not editing everything, you can get rid of all the tags:

<Grid><RichTextBox><FlowDocument><Paragraph><Figure>

In the data template. You probably can't bind the Text of the message to the contentText in the DataTemplate, and will have to have a bit of behind-the-scenes code to dynamically generate the inlines for the TextBlock.

Addition 3 - How to bind a TextBlock to contain images etc from XAML

Okay, so overall (neglecting some styling), I suggest the following:

<DataTemplate x:Key="MessageDataTemplate" DataType="{x:Type classes:Message}">
    <Border>
        <Border>
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="150"/>
                    <ColumnDefinition Width="80"/>
                </Grid.ColumnDefinitions>
                <Grid.RowDefinitions>
                    <RowDefinition Height="15"/>
                    <RowDefinition Height="Auto"/>
                </Grid.RowDefinitions>
                <TextBlock x:Name="tUser"
                    Foreground="Gray"
                    TextAlignment="Right"
                    FontSize="10"
                    Grid.Row="0"
                    Grid.Column="1"
                    Text="{Binding displayUserName}" />
                <TextBlock x:Name="tTime"
                    Foreground="Gray"
                    TextAlignment="Left"
                    FontSize="10"
                    Grid.Row="0"
                    Grid.Column="0"
                    Text="{Binding sendTime}" />
                <TextBlock x:Name="tMessage"
                    Foreground="Black"
                    TextAlignment="Justify"
                    FontSize="12"
                    Height="NaN"
                    TextWrapping="Wrap"
                    Grid.Row="1"
                    Grid.Column="0"
                    Grid.ColumnSpan="2"
                    classes:TextBlockInlineBinder.Inlines="{Binding contentInlines}" />
                <Image x:Name="tImage"
                    Grid.Row="1"
                    Grid.Column="0"
                    Grid.ColumnSpan="2"
                    Height="NaN"
                    Source="{Binding imageSend}" />
            </Grid>
        </Border>
    </Border>
</DataTemplate>

Note the line classes:TextBlockInlineBinder.Inlines="{Binding contentInlines}" on the message TextBlock. This is in order to be able to bind to Inlines... Basically, this not a dependency property, so cannot be directly bound to!

Instead, we can use the custom static class TextBlockInlineBinder below to create a static dependency property to add to our TextBlock, which when it is updated, it runs the InlinesChanged method to update the Inlines:

public static class TextBlockInlineBinder
{
    #region Static DependencyProperty Implementation

    public static readonly DependencyProperty InlinesProperty =
        DependencyProperty.RegisterAttached("Inlines",
        typeof(IEnumerable<Inline>),
        typeof(TextBlockInlineBinder),
        new UIPropertyMetadata(new Inline[0], InlinesChanged));

    public static string GetInlines(DependencyObject obj)
    {
        return (string)obj.GetValue(InlinesProperty);
    }

    public static void SetInlines(DependencyObject obj, string value)
    {
        obj.SetValue(InlinesProperty, value);
    }

    #endregion

    private static void InlinesChanged(DependencyObject sender, 
                                       DependencyPropertyChangedEventArgs e)
    {
        var value = e.NewValue as IEnumerable<Inline>;
        var textBlock = sender as TextBlock;
        textBlock.Inlines.Clear();
        textBlock.Inlines.AddRange(value);
    }
} 

Finally, the binding (which I've bound to a contentInlines property on your Message class) will need to be of type IEnumerable<Inline>, ie something like:

public IEnumerable<Inline> contentInlines
{
    get {
        var inlines = new List<Inline>();
        for (var i = 0; i < _mess.data.Length; i += Char.IsSurrogatePair(_mess.data, i) ? 2 : 1)
        {
            var x = Char.ConvertToUtf32(_mess.data, i);

            if (EmojiConverter.EmojiDictionary.ContainsKey(x))
            {
                //Generate new Image from Emoji
                var emoticonImage = new Image
                {
                    Width = 20,
                    Height = 20,
                    Margin = new Thickness(0, -5, 0, -5),
                    Source = EmojiConverter.EmojiDictionary[x]
                };
                inlines.Add(emoticonImage);
            }
            else
            {
                inlines.Add(new Run("" + _mess.data[i]));
            }
        }
        return inlines;
    }
}
Community
  • 1
  • 1
David E
  • 1,384
  • 9
  • 14
  • By the sound of it, no matter how you display the messages (though in a rich text box wouldn't be the best way - if you're just including smileys, potentially dynamically generated text blocks and image tags might take a lot less time to load(!)), you'll want some form of virtualization if you could have 20000 messages in a conversation(!) – David E Sep 11 '14 at 16:31
  • So you mean i should create for example textbox(bla bla)textbox(png) textbox(bla bla) ? And this in a ItemsControl so I can scroll? It should always possible to scroll the hole text. For a better understanding you can imaging it like whatsapp or similar. – nefas Sep 11 '14 at 16:41
  • if you just want to be able to *display*, not edit the text, then textblocks and images would be better and require less overhead. If you want to be able to edit each individual post, and move images around etc, then maybe a RichTextBox might be easier. (though you have to consider whether you actually want the user to be able to do all the powerful things a RTB lets them do - eg make things bold with `Ctrl+B` etc etc) – David E Sep 11 '14 at 16:55
  • The above code is to work with a `ListBox`, but a similar thing can be done with an `ItemsControl` as per the link in my comment - to be honest, `ListBox` is probably easier to use :). The default virtualization done is so that as many whole items will be on-screen at any one time, but no part items. If you wish to have smooth scrolling AND virtualization, I've just read about a `TreeView` eg here http://classpattern.com/smooth-scrolling-with-virtualization-wpf-list.html#.VBHVu_ldXSg - though can't vouch for it myself :) – David E Sep 11 '14 at 17:04
  • @nefas - sorry, that should be Text*Block*s - ie http://msdn.microsoft.com/en-us/library/system.windows.controls.textblock(v=vs.110).aspx SO *basically* all you need to do is get rid of the tags:
    And start your DataTemplate by just using the first Border tag.
    – David E Sep 11 '14 at 17:13
  • hey ;) the working speed is now perfekt. But at the moment without smileys and pictures instead of text. How dose you mean it with "behind-the-scenes code" like a Converter that is called and transform the unicode to Emoticons? – nefas Sep 11 '14 at 17:55
  • Okay, updated the answer with a method to do it. Spent a while looking into the cleanest way, think that's it :) – David E Sep 11 '14 at 21:25