1

I'm trying in every way to get out of the loop that must create multiple RichTextBlockOverflow controls based on arbitrary input text length but without success. The HasOverflowContent property doesn't update either synchronously or asynchronously.

The variable bool "ThereIsText" I can not understand when and how to make it false to stop the loop.

The link with the text to paste in the paragraph "Run" is: text to paste.

MainPage.xaml:

<Page
x:Class="Text_Viewer_Test_UWP.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Text_Viewer_Test_UWP"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <Grid x:Name="Menù" HorizontalAlignment="Left" Width="290" Padding="0" Margin="0,21,0,0">
        <Grid Background="White">
            <Grid.RowDefinitions>
                <RowDefinition Height="50"/>
                <RowDefinition Height="50"/>
                <RowDefinition Height="50"/>
            </Grid.RowDefinitions>
            <Button  Grid.Row="0" x:Name="btnLoadText" Click="btnLoadText_Click" Content="Display text" HorizontalAlignment="Center" VerticalAlignment="Center" Width="270" Foreground="White" Height="32"/>
            <TextBlock Grid.Row="1" x:Name="txtPage" HorizontalAlignment="Center" VerticalAlignment="Center"/>
        </Grid>
    </Grid>
    <Grid x:Name="BaseGrid" Margin="320,10,30,10" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="Black">
        <ScrollViewer x:Name="PageViewer" Background="White" VerticalScrollBarVisibility="Disabled" HorizontalScrollBarVisibility="Visible" VerticalScrollMode="Disabled" HorizontalScrollMode="Enabled">
            <StackPanel x:Name="StackViewer" VirtualizingStackPanel.VirtualizationMode="Recycling" Orientation="Horizontal"/>
        </ScrollViewer>
    </Grid>
</Grid>

MainPage.xaml.cs:

public sealed partial class MainPage : Page
{
    RichTextBlock TextOneRich = new RichTextBlock() { Margin = new Thickness(20) };
    List<RichTextBlockOverflow> TextList = new List<RichTextBlockOverflow>();
    bool ThereIsText = true;

    public MainPage()
    {
        this.InitializeComponent();

        StackViewer.Children.Add(TextOneRich);
        TextOneRich.Width = 400;
        TextOneRich.TextAlignment = TextAlignment.Justify;

    }

    private async void btnLoadText_Click(object sender, RoutedEventArgs e)
    {
        TextList.Clear();
        TextOneRich.Blocks.Clear();
        StackViewer.Children.Clear();
        StackViewer.Children.Add(TextOneRich);


        Paragraph paragraphText = new Paragraph();
        paragraphText.Inlines.Clear();
        paragraphText.Inlines.Add(new Run { Text = "PasteTextHere" });


        await Task.Run(async () =>
        {
            await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
            {
                TextOneRich.Blocks.Add(paragraphText);
            });
        }).ContinueWith(async t =>
        {
            await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
            {
                TextList.Add(new RichTextBlockOverflow() { Width = 400, Margin = new Thickness(20) });
                StackViewer.Children.Add(TextList[0]);
                TextOneRich.OverflowContentTarget = TextList[0];
            });
        });

        await Task.Run(async () =>
        {
            await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, async () =>
            {
                while (ThereIsText)
                {
                    await Task.Run(async () =>
                    {
                        await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
                        {
                            TextList.Add(new RichTextBlockOverflow() { Width = 400, Margin = new Thickness(20) });
                            StackViewer.Children.Add(TextList[TextList.Count - 1]);
                            TextList[TextList.Count - 2].OverflowContentTarget = TextList[TextList.Count - 1];
                            txtPage.Text = TextList.Count.ToString();
                        });
                    });
                }
            });
        });
    }
}
Peter Torr - MSFT
  • 11,824
  • 3
  • 18
  • 51
Tibrus
  • 37
  • 5
  • 2
    Why are you creating so many tasks that just dispatch back to the UI again? Have you tried writing the code without any `await` / `async` code? – Peter Torr - MSFT Dec 13 '17 at 03:25
  • Yes, I tried, but I blocked the application. To allow the "loading" of the RichTextBlockOverflow without blocking the application it is necessary to use await - async. At least I tried again and again I found this solution. How can it be done? – Tibrus Dec 13 '17 at 09:00
  • Could you give me an example of what you mean without await - async? – Tibrus Dec 13 '17 at 10:21

1 Answers1

1

If you need to do a lot of manipulation of UI objects, and you want to keep the UI responsive while you do that(1) then you can generally just await for a single millisecond, which will allow the UI to process any input messages etc.

Trying to access the HasOverflowContent property is problematic since it requires a layout pass to complete, and that could take an arbitrary amount of time. We could just await an arbitrary amount of time - say 50ms - but that wouldn't be ideal. Instead, you can use a technique similar to the one from "Waiting for XAML layout to complete" with a slight adjustment.

This XAML and code adds 1000 lines of text to a set of RichTextBlock / RichTextBlockOverflow controls and does so while keeping the UI responsive (the ball keeps moving, and you can scroll the list at any time):

XAML:

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
  <StackPanel Margin="20" HorizontalAlignment="Stretch" x:Name="container">
    <Ellipse Width="20" Height="20" Margin="0, 5" Fill="red"
             x:Name="animation" HorizontalAlignment="Left"/>
    <Button Content="Go" Click="Go" Margin="0,0,0,5"/>
    <ScrollViewer MaxHeight="500">
      <StackPanel x:Name="thePanel"/>
    </ScrollViewer>
  </StackPanel>
</Grid>

Code:

public static class Extensions
{
  // This helper function is essentially the same as this answer:
  // https://stackoverflow.com/a/14132711/4184842
  //
  // It adds an additional forced 1ms delay to let the UI thread
  // catch up.
  public static Task FinishLayoutAsync(this FrameworkElement element)
  {
    TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();

    // Setup handler that will be raised when layout has completed.
    EventHandler<object> handler = null;
    handler = (s, a) =>
    {
      element.LayoutUpdated -= handler;
      tcs.SetResult(true);
    };
    element.LayoutUpdated += handler;

    // Await at least 1 ms (to force UI pump) and until the Task is completed
    // If you don't wait the 1ms then you can get a 'layout cycle detected' error
    // from the XAML runtime.
    return Task.WhenAll(new[] { Task.Delay(1), tcs.Task });
  }
}

public sealed partial class MainPage : Page
{
  public MainPage()
  {
    InitializeComponent();

    // Simple animation to show the UI is not frozen
    BadUiAnimation_DontDoThis();
  }

  // Very VERY bad way of doing animation, but it shows
  // that the UI is still responsive. Normally you should
  // use StoryBoards to do animation.
  void BadUiAnimation_DontDoThis()
  {
    DispatcherTimer dt = new DispatcherTimer();
    dt.Interval = TimeSpan.FromMilliseconds(33);
    int delta = 4;
    const int width = 20;
    dt.Tick += (s, a) =>
    {
      var leftOffset = animation.Margin.Left;
      if (leftOffset + delta < 0)
      {
        delta *= -1;
        leftOffset = 0;
      }
      else if (leftOffset + delta + width > container.ActualWidth)
      {
        delta *= -1;
        leftOffset = container.ActualWidth - width;
      }
      else
      {
        leftOffset += delta;
      }

      animation.Margin = new Thickness(leftOffset, 5, 0, 5);
    };
    dt.Start();
  }

  private async void Go(object sender, RoutedEventArgs e)
  {
    // Helper function
    void AppendSimpleString(string s)
    {
      RichTextBlock rtb = new RichTextBlock();
      rtb.Blocks.Add(CreateParagraphWithText(s));
      thePanel.Children.Add(rtb);
    }

    // Another helper function
    Paragraph CreateParagraphWithText(string s)
    {
      var p = new Paragraph();
      var r = new Run();
      r.Text = s;
      p.Inlines.Add(r);
      return p;
    }

    // Disable the button so you can't click it again until the 
    // insertion is over
    (sender as Button).IsEnabled = false;
    thePanel.Children.Clear();

    AppendSimpleString($"Begin...{Environment.NewLine}");

    // Generate some dummy strings to add to the page
    var strings = new StringBuilder();
    for (int i = 0; i < 1000; i++)
      strings.Append($"This is line {i + 1}{Environment.NewLine}");

    string text = strings.ToString();

    // Create initial block with far too much text in it
    var source = new RichTextBlock();
    source.MaxHeight = 100;
    source.Blocks.Add(CreateParagraphWithText(text));
    thePanel.Children.Add(source);

    // Create the first overflow and connect it to the original textblock
    var prev = new RichTextBlockOverflow
    {
      MaxHeight = 100,
      Margin = new Thickness(0, 10, 0, 0)
    };
    thePanel.Children.Add(prev);
    source.OverflowContentTarget = prev;

    // Wait for layout to complete so we can check the 
    // HasOverflowContent property
    await prev.FinishLayoutAsync();

    // Keep creating more overflows until there is no content left
    while (prev.HasOverflowContent)
    {
      var next = new RichTextBlockOverflow
      {
        MaxHeight = 100,
        Margin = new Thickness(0, 10, 0, 0)
      };
      thePanel.Children.Add(next);
      prev.OverflowContentTarget = next;

      // Wait for layout to complete, which will compute whether there
      // is additional overflow (or not)
      await prev.FinishLayoutAsync();

      prev = next;
    };

    AppendSimpleString($"Done!{Environment.NewLine}");

    // Enable interaction with the button again
    (sender as Button).IsEnabled = true;
  }
}

(1): Note that you probably want to do something to limit interaction with your UI while this is happening, which might require you to disable some controls or otherwise make sure the user doesn't mess with your app's state. The sample does this by disabling and then re-enabling the button.

Community
  • 1
  • 1
Peter Torr - MSFT
  • 11,824
  • 3
  • 18
  • 51
  • What you told me is right and I could do it, but for my work I do not know how many times I have to repeat the cycle. The while will need to be repeated until there is still text in order to create more RichTextBlockOverflow; and will have to exit the loop (so stop) when the text is finished. And this can be done using the HasOverflowContent property of the RichTexBlockOverflow; but I do not know how I can do it. Some way to solve? Alwais Thanks. – Tibrus Dec 14 '17 at 17:23
  • Updated answer to handle the case of layout-dependent properties. – Peter Torr - MSFT Dec 18 '17 at 00:34