Изчакайте, докато контролното оформление приключи

Зареждам доста богат текст в RichTextBox (WPF) и искам да превъртя до края на съдържанието:

richTextBox.Document.Blocks.Add(...)
richTextBox.UpdateLayout();
richTextBox.ScrollToEnd();

Това не работи, ScrollToEnd се изпълнява, когато оформлението не е завършено, така че не се превърта до края, а се превърта до около първата трета от текста.

Има ли начин да принудите да изчакате, докато RichTextBox завърши своите операции по рисуване и оформление, така че ScrollToEnd действително да превърти до края на текста?

Благодаря.

Неща, които не работят:

РЕДАКТИРАНЕ: Опитах събитието LayoutUpdated, но то се задейства веднага, същият проблем: контролата все още излага повече текст вътре в полето с богат текст, когато се задейства, така че дори ScrollToEnd там не работи... опитах това:

richTextBox.Document.Blocks.Add(...)
richTextBoxLayoutChanged = true;
richTextBox.UpdateLayout();
richTextBox.ScrollToEnd();

и вътре в манипулатора на събития richTextBox.LayoutUpdated:

if (richTextBoxLayoutChanged)
{
    richTextBoxLayoutChanged = false;
    richTextBox.ScrollToEnd();
}

Събитието се задейства правилно, но твърде рано, richtextbox все още добавя още текст, когато се задейства, оформлението не е завършено, така че ScrollToEnd отново се проваля.

РЕДАКТИРАНЕ 2: След отговора на dowhilefor: MSDN на InvalidateArrange казва

След анулирането оформлението на елемента ще бъде актуализирано, което ще се случи асинхронно, освен ако впоследствие не бъде принудено от UpdateLayout.

И все пак дори

richTextBox.InvalidateArrange();
richTextBox.InvalidateMeasure();
richTextBox.UpdateLayout();

НЕ чака: след тези извиквания richtextbox все още добавя още текст и го поставя вътре в себе си асинхронно. ARG!


person SemMike    schedule 07.07.2011    source източник
comment
Моля, спрете да пишете тагове в заглавията.   -  person Lightness Races in Orbit    schedule 17.07.2011


Отговори (8)


Разгледайте UpdateLayout

особено:

Извикването на този метод няма ефект, ако оформлението е непроменено или ако нито подреждането, нито състоянието на измерване на оформлението са невалидни

Така че извикването на InvalidateMeasure или InvalidateArrange, в зависимост от вашите нужди, трябва да работи.

Но като се има предвид вашият код. Мисля, че това няма да работи. Голяма част от зареждането и създаването на WPF е отложено, така че добавянето на нещо към Document.Blocks не променя непременно директно потребителския интерфейс. Но трябва да кажа, че това е само предположение и може би греша.

person dowhilefor    schedule 07.07.2011
comment
Благодаря, не работи, но си струваше да опитаме. Дори извикването на InvalidateArrange и InvalidateMeasure преди UpdateLayout не решава проблема, richtextbox все още добавя/оформя текста асинхронно след всички тези извиквания. Не чака. - person SemMike; 07.07.2011

Имах свързана ситуация: имам диалогов прозорец за предварителен преглед на печат, който създава фантастично изобразяване. Обикновено потребителят ще щракне върху бутон, за да го отпечата, но аз също исках да го използвам, за да запазя изображение без участието на потребителя. В този случай създаването на изображението трябва да изчака завършването на оформлението.

Успях да използвам следното:

Dispatcher.Invoke(new Action(() => {SaveDocumentAsImage(....);}), DispatcherPriority.ContextIdle);

Ключът е DispatcherPriority.ContextIdle, който изчаква изпълнението на фоновите задачи.

Редактиране: По искане на Зак, включително кода, приложим за този конкретен случай:

Dispatcher.Invoke(() => { richTextBox.ScrollToEnd(); }), DispatcherPriority.ContextIdle);

Трябва да отбележа, че не съм много доволен от това решение, тъй като се чувства невероятно крехко. Изглежда обаче, че работи в моя конкретен случай.

person Patrick Simpson    schedule 09.02.2015
comment
Патрик, това е чудесно решение. Открих, че тази работа работи най-добре сред всички решения, дадени тук. Dispatcher.Invoke(() => { richTextBox.ScrollToEnd(); }), DispatcherPriority.ContextIdle); Бихте ли, моля, актуализирайте отговора си, за да включите този код, за да се вижда по-добре? - person Zach; 22.04.2015
comment
Благодаря ви за този отговор! Това също ми помогна в случай, когато получавах PropertyChanged събития на персонализирана контрола, произхождащи от персонализирани свойства на зависимост, които бяха извикани след свойствата на зависимост на стандартни контроли. Не съм сигурен дали съм направил грешка някъде при внедряването им или дали това е свързано с обвързванията. Както и да е, Dispatcher.Invoke без посочване на приоритет винаги ще се изпълнява ПРЕДИ тези PropertyChanged събития от моите персонализирани свойства на зависимост. Използвайки DispatcherPriority.ContextIdle, изведнъж делегатът на диспечера се изпълнява СЛЕД тях. - person j00hi; 09.12.2017
comment
Използването на Dispatcher.Invoke с DispatcherPriority.ContextIdle причинява визуално забавяне след зареждането на контролите, преди да се приложи допълнителното действие. Вместо това използването на await Task.Yield() не причинява такова визуално забавяне. - person Etienne Charland; 12.07.2018

трябва да можете да използвате събитието Loaded

ако правите това повече от веднъж, тогава трябва да погледнете LayoutUpdated събитие

myRichTextBox.LayoutUpdated += (source,args)=> ((RichTextBox)source).ScrollToEnd();
person Muad'Dib    schedule 07.07.2011
comment
същата забележка: благодаря, но генерирам този дълъг текст много пъти по време на изпълнение, не само когато се зарежда richtextbox (текстът се генерира от заявки към база данни)... - person SemMike; 07.07.2011
comment
@SemMike ахххх, пропуснал си ТОВА малко информация :) - person Muad'Dib; 07.07.2011
comment
благодаря, но вече опитах събитието UpdateLayout (следвайки предложенията Loaded), вижте първата ми редакция... - person SemMike; 07.07.2011

С .net 4.5 или пакета async blc можете да използвате следния метод за разширение

 /// <summary>
    /// Async Wait for a Uielement to be loaded
    /// </summary>
    /// <param name="element"></param>
    /// <returns></returns>
    public static Task WaitForLoaded(this FrameworkElement element)
    {
        var tcs = new TaskCompletionSource<object>();
        RoutedEventHandler handler = null;
        handler = (s, e) =>
        {
            element.Loaded -= handler;
            tcs.SetResult(null);
        };
        element.Loaded += handler;
        return tcs.Task;
    }
person Andreas    schedule 20.04.2016
comment
Това работи добре, но може да увисне (на теория). Публикувах подобрена версия на този код. - person Contango; 16.05.2016

Отговорът на @Andreas работи добре.

Но какво ще стане, ако контролата вече е заредена? Събитието никога няма да се задейства и чакането потенциално ще продължи вечно. За да коригирате това, върнете се незабавно, ако формулярът вече е зареден:

/// <summary>
/// Intent: Wait until control is loaded.
/// </summary>
public static Task WaitForLoaded(this FrameworkElement element)
{
    var tcs = new TaskCompletionSource<object>();
    RoutedEventHandler handler = null;
    handler = (s, e) =>
    {
        element.Loaded -= handler;
        tcs.SetResult(null);
    };
    element.Loaded += handler;

    if (element.IsLoaded == true)
    {
        element.Loaded -= handler;
        tcs.SetResult(null);
    }
        return tcs.Task;
}

Допълнителни съвети

Тези съвети могат или не могат да бъдат полезни.

  • Кодът по-горе е наистина полезен в прикачено свойство. Прикачено свойство се задейства само ако стойността се промени. Когато превключвате прикаченото свойство, за да го задействате, използвайте task.Yield(), за да поставите повикването в задната част на опашката на диспечера:

    await Task.Yield(); // Put ourselves to the back of the dispatcher queue.
    PopWindowToForegroundNow = false;
    await Task.Yield(); // Put ourselves to the back of the dispatcher queue.
    PopWindowToForegroundNow = false;
    
  • Кодът по-горе е наистина полезен в прикачено свойство. Когато превключвате прикаченото свойство, за да го задействате, можете да използвате диспечера и да зададете приоритета на Loaded:

    // Ensure PopWindowToForegroundNow is initialized to true
    // (attached properties only trigger when the value changes).
    Application.Current.Dispatcher.Invoke(
    async 
       () =>
    {
        if (PopWindowToForegroundNow == false)
        {
           // Already visible!
        }
        else
        {
            await Task.Yield(); // Put ourselves to the back of the dispatcher queue.
            PopWindowToForegroundNow = false;
        }
    }, DispatcherPriority.Loaded);
    
person Contango    schedule 16.05.2016

Опитайте да добавите своя richTextBox.ScrollToEnd(); извикване на манипулатора на събития LayoutUpdated на вашия обект RichTextBox.

person Philipp Schmid    schedule 07.07.2011
comment
Това е подвеждащо. И мисля, че много хора все още смятат, че това събитие се извиква само когато се промени оформлението на контролата, където е добавен слушателят. Но това не е така. LayoutUpdated се извиква, когато КОЙТО и да е елемент промени оформлението си. Разгледайте тук - person dowhilefor; 07.07.2011

Опитайте тази:

richTextBox.CaretPosition = richTextBox.Document.ContentEnd;
richTextBox.ScrollToEnd(); // maybe not necessary
person Pollitzer    schedule 12.01.2015

Единственото (kludge) решение, което работи за моя WPF проект, беше да пусна отделна нишка, която спя за известно време и след това поиска да превърти до края.

Важно е да не се опитвате да извиквате тази "заспала" нишка в основния GUI, за да не бъде поставен на пауза потребителят. Затова извиквайте отделна "заспала" нишка и периодично Dispatcher. Извиквайте основната GUI нишка и помолете да превъртите до края.

Работи перфектно и потребителското изживяване не е ужасно:

using System;
using System.Threading;    
using System.Windows.Controls;

try {

    richTextBox.ScrollToEnd();

    Thread thread       = new Thread(new ThreadStart(ScrollToEndThread));
    thread.IsBackground = true;
    thread.Start();

} catch (Exception e) {

    Logger.Log(e.ToString());
}

и

private void ScrollToEndThread() {

// Using this thread to invoke scroll to bottoms on rtb
// rtb was loading for longer than 1 second sometimes, so need to 
// wait a little, then scroll to end
// There was no discernable function, ContextIdle, etc. that would wait
// for all the text to load then scroll to bottom
// Without this, on target machine, it would scroll to the bottom but the
// text was still loading, resulting in it scrolling only part of the way
// on really long text.
// Src: https://stackoverflow.com/questions/6614718/wait-until-control-layout-is-finished
    for (int i=1000; i <= 10000; i += 3000) {

        System.Threading.Thread.Sleep(i);

        this.richTextBox.Dispatcher.Invoke(
            new ScrollToEndDelegate(ScrollToEnd),
            System.Windows.Threading.DispatcherPriority.ContextIdle,
            new object[] {  }
            );
    }
}

private delegate void ScrollToEndDelegate();

private void ScrollToEnd() {

    richTextBox.ScrollToEnd();
}
person Mike    schedule 15.02.2019