Де Application.DoEvents () у WPF?


89

У мене є такий зразок коду, який збільшується при кожному натисканні кнопки:

XAML:

<Window x:Class="WpfApplication12.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">

    <Canvas x:Name="myCanvas">

        <Canvas.LayoutTransform>
            <ScaleTransform x:Name="myScaleTransform" />
        </Canvas.LayoutTransform> 

        <Button Content="Button" 
                Name="myButton" 
                Canvas.Left="50" 
                Canvas.Top="50" 
                Click="myButton_Click" />
    </Canvas>
</Window>

* .cs

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private void myButton_Click(object sender, RoutedEventArgs e)
    {
        Console.WriteLine("scale {0}, location: {1}", 
            myScaleTransform.ScaleX,
            myCanvas.PointToScreen(GetMyByttonLocation()));

        myScaleTransform.ScaleX =
            myScaleTransform.ScaleY =
            myScaleTransform.ScaleX + 1;

        Console.WriteLine("scale {0}, location: {1}",
            myScaleTransform.ScaleX,
            myCanvas.PointToScreen(GetMyByttonLocation()));
    }

    private Point GetMyByttonLocation()
    {
        return new Point(
            Canvas.GetLeft(myButton),
            Canvas.GetTop(myButton));
    }
}

Вихід:

scale 1, location: 296;315
scale 2, location: 296;315

scale 2, location: 346;365
scale 3, location: 346;365

scale 3, location: 396;415
scale 4, location: 396;415

як бачите, існує проблема, яку я думав вирішити за допомогою, Application.DoEvents();але ... вона не існує апріорі в .NET 4.

Що робити?


8
Різьбонарізання? Application.DoEvents () замінив бідноту належним чином писати багатопотокові програми та надзвичайно погану практику в будь-якому випадку.
Колін Маккей,

2
Я знаю, що це погано і погано, але я віддаю перевагу тому, що взагалі нічого.
serhio

Відповіді:


25

Старий метод Application.DoEvents () застарів у WPF на користь використання диспетчера або фонової робочої нитки для обробки, як ви описали. Дивіться посилання на кілька статей про те, як використовувати обидва об’єкти.

Якщо вам обов’язково потрібно використовувати Application.DoEvents (), ви можете просто імпортувати system.windows.forms.dll у свою програму та викликати метод. Однак це дійсно не рекомендується, оскільки ви втрачаєте всі переваги, які надає WPF.


Я знаю, що це погано і погано, але я віддаю перевагу тому, що взагалі нічого ... Як я можу використовувати Dispatcher у своїй ситуації?
serhio

3
Я розумію вашу ситуацію. Я був у ньому, коли писав свою першу програму WPF, але я пішов і вивчив нову бібліотеку, і в довгостроковій перспективі для неї було набагато краще. Настійно рекомендую приділити час. Що стосується вашого конкретного випадку, мені здається, що ви хотіли б, щоб диспетчер обробляв відображення координат кожного разу, коли запускається ваша подія натискання. Вам потрібно буде прочитати більше про Диспетчер для точної реалізації.
Діллі-О

ні, я би зателефонував Application.DoEventsпісля збільшення myScaleTransform.ScaleX. Не знаю, чи можливо це з Диспетчером.
serhio

12
Видалення Application.DoEvents () майже так само дратує, як MS, видаляючи кнопку "Пуск" у Windows 8.
JeffHeaton

2
Ні, це було добре, що цей метод завдав більше шкоди, ніж користі.
Джессі,

136

Спробуйте щось подібне

public static void DoEvents()
{
    Application.Current.Dispatcher.Invoke(DispatcherPriority.Background,
                                          new Action(delegate { }));
}

1
Я навіть написав метод розширення для застосування :)public static void DoEvents(this Application a)
serhio

@serhio: Акуратний метод розширення :)
Фредрік Хедблад,

2
Однак слід зауважити, що в реальному додатку Application.Currentінколи нуль ... тому, можливо, він не зовсім еквівалентний.
serhio

Ідеально Це має бути відповіддю. Найпростіші не повинні отримувати кредит.
Jeson Martajaya

6
Це не завжди спрацьовує, оскільки воно не штовхає фрейм, якщо виконується перериваюча інструкція (тобто виклик методу WCF, який у синхроніці продовжується до цієї команди), швидше за все, ви не побачите 'оновлення', оскільки вона буде заблокована .. Ось чому відповідь flq, надана з ресурсу MSDN, є більш правильною, ніж ця.
GY

57

Ну, я просто потрапив у випадок, коли я починаю роботу над методом, який працює на потоці Dispatcher, і йому потрібно блокувати, не блокуючи потоку інтерфейсу користувача. Виявляється, msdn пояснює, як реалізувати DoEvents () на основі самого диспетчера:

public void DoEvents()
{
    DispatcherFrame frame = new DispatcherFrame();
    Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background,
        new DispatcherOperationCallback(ExitFrame), frame);
    Dispatcher.PushFrame(frame);
}

public object ExitFrame(object f)
{
    ((DispatcherFrame)f).Continue = false;

    return null;
}

(взято з Dispatcher.PushFrame Method )

Деякі можуть віддати перевагу одному методу, який застосовуватиме ту саму логіку:

public static void DoEvents()
{
    var frame = new DispatcherFrame();
    Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background,
        new DispatcherOperationCallback(
            delegate (object f)
            {
                ((DispatcherFrame)f).Continue = false;
                return null;
            }),frame);
    Dispatcher.PushFrame(frame);
}

Приємна знахідка! Це виглядає безпечнішим, ніж запропоноване впровадження Meleak. Я знайшов допис у блозі про це
HugoRune

2
@HugoRune У цій публікації в блозі зазначено, що такий підхід непотрібний, і використовувати ту саму реалізацію, що і Meleak.
Луказоїд

1
@Lukazoid Наскільки я можу зрозуміти, проста реалізація може спричинити важко відстежувані блокування. (Я не впевнений у причині, можливо проблема полягає в коді в черзі диспетчера, який знову викликає DoEvents, або в коді в черзі диспетчера, що генерує подальші кадри диспетчера.) У будь-якому випадку рішення з exitFrame не виявляло таких проблем, тому я б рекомендую це. (Або, звичайно, взагалі не використовувати doEvents)
HugoRune

1
Показ накладеного вікна замість діалогового вікна у поєднанні із способом caliburn залучити віртуальні машини, коли програма закривається, виключає зворотні виклики та вимагає блокування без блокування. Я був би радий, якщо б ви запропонували мені рішення без злому DoEvents.
flq

1
нове посилання на стару публікацію в блозі: kent-boogaart.com/blog/dispatcher-frames
CAD хлопець

12

Якщо вам потрібно просто оновити графіку вікна, краще скористайтеся таким

public static void DoEvents()
{
    Application.Current.Dispatcher.Invoke(DispatcherPriority.Render,
                                          new Action(delegate { }));
}

1
Використовуючи DispatcherPriority.Render працюйте швидше, ніж DispatcherPriority.Background. Перевірено сьогодні за допомогою StopWatcher
Александр Пекшев

Я щойно використав цей трюк, але використовую DispatcherPriority.Send (найвищий пріоритет) для найкращих результатів; більш чуйний інтерфейс.
Бент Расмуссен,

6
myCanvas.UpdateLayout();

здається, це теж працює.


Я буду використовувати це, оскільки це здається мені набагато безпечнішим, але я збираюся зберегти DoEvents для інших випадків.
Картер Медлін

Не знаю чому, але це для мене не працює. DoEvents () чудово працює.
newman

У моєму випадку мені довелося зробити це так само, як і DoEvents ()
Джефф

3

Однією з проблем обох запропонованих підходів є те, що вони спричиняють простою використання центрального процесора (до 12%, на мій досвід). У деяких випадках це неоптимально, наприклад, коли поведінка модального інтерфейсу реалізована за допомогою цієї техніки.

Наступний варіант вводить мінімальну затримку між кадрами за допомогою таймера (зауважте, що це написано тут із Rx, але може бути досягнуто за допомогою будь-якого звичайного таймера):

 var minFrameDelay = Observable.Interval(TimeSpan.FromMilliseconds(50)).Take(1).Replay();
 minFrameDelay.Connect();
 // synchronously add a low-priority no-op to the Dispatcher's queue
 Application.Current.Dispatcher.Invoke(DispatcherPriority.Background, new Action(() => minFrameDelay.Wait()));

1

З моменту введення asyncі awaitтепер можна відмовитися від потоку інтерфейсу частково через (раніше) * синхронний блок коду, використовуючи Task.Delay, наприклад

private async void myButton_Click(object sender, RoutedEventArgs e)
{
    Console.WriteLine("scale {0}, location: {1}", 
        myScaleTransform.ScaleX,
        myCanvas.PointToScreen(GetMyByttonLocation()));

    myScaleTransform.ScaleX =
        myScaleTransform.ScaleY =
        myScaleTransform.ScaleX + 1;

    await Task.Delay(1); // In my experiments, 0 doesn't work. Also, I have noticed
                         // that I need to add as much as 100ms to allow the visual tree
                         // to complete its arrange cycle and for properties to get their
                         // final values (as opposed to NaN for widths etc.)

    Console.WriteLine("scale {0}, location: {1}",
        myScaleTransform.ScaleX,
        myCanvas.PointToScreen(GetMyByttonLocation()));
}

Буду чесним, я не пробував його з точним наведеним вище кодом, але я використовую його в щільних циклах, коли розміщую багато предметів у ItemsControlякому є дорогий шаблон елемента, іноді додаючи невелику затримку, щоб дати іншому речі на інтерфейсі більше часу.

Наприклад:

        var levelOptions = new ObservableCollection<GameLevelChoiceItem>();

        this.ViewModel[LevelOptionsViewModelKey] = levelOptions;

        var syllabus = await this.LevelRepository.GetSyllabusAsync();
        foreach (var level in syllabus.Levels)
        {
            foreach (var subLevel in level.SubLevels)
            {
                var abilities = new List<GamePlayingAbility>(100);

                foreach (var g in subLevel.Games)
                {
                    var gwa = await this.MetricsRepository.GetGamePlayingAbilityAsync(g.Value);
                    abilities.Add(gwa);
                }

                double PlayingScore = AssessmentMetricsProcessor.ComputePlayingLevelAbility(abilities);

                levelOptions.Add(new GameLevelChoiceItem()
                    {
                        LevelAbilityMetric = PlayingScore,
                        AbilityCaption = PlayingScore.ToString(),
                        LevelCaption = subLevel.Name,
                        LevelDescriptor = level.Ordinal + "." + subLevel.Ordinal,
                        LevelLevels = subLevel.Games.Select(g => g.Value),
                    });

                await Task.Delay(100);
            }
        }

У магазині Windows, коли приємний перехід теми в колекції, ефект є цілком бажаним.

Лука

  • дивіться коментарі. Коли я швидко писав свою відповідь, я думав про те, як взяти синхронний блок коду, а потім віддати потік назад своєму абоненту, ефект якого робить блок коду асинхронним. Я не хочу повністю переформулювати свою відповідь, тому що тоді читачі не можуть зрозуміти, про що ми з Серві сварились.

"тепер можна відмовитися від потоку інтерфейсу частково через синхронно блокований" Ні, це не так. Ви щойно зробили код асинхронним , а не перекачуєте повідомлення з потоку інтерфейсу користувача синхронним методом. Тепер правильно розробленою програмою WPF буде програма, яка ніколи не блокує потік інтерфейсу, синхронно виконуючи тривалі операції в потоці інтерфейсу, в першу чергу, використовуючи асинхронність, щоб дозволити існуючому насосу повідомлень накачати повідомлення належним чином.
Серві

@Servy Під обкладинками awaitкомпілятор підпише решту методів асинхронізації як продовження очікуваного завдання. Це продовження відбуватиметься в потоці інтерфейсу користувача (той же контекст синхронізації). Потім керування повертається до абонента методу асинхронізації, тобто підсистеми подій WPF, де події будуть виконуватися до тих пір, поки заплановане продовження не пройде десь після закінчення періоду затримки.
Люк Пуплетт

так, я це добре знаю. Ось що робить метод асинхронним (передача контролю для абонента і лише планування продовження). У вашій відповіді зазначено, що метод є синхронним, коли він фактично використовує асинхронність для оновлення інтерфейсу.
Серві

Перший метод (код OP) є синхронним, Servy. Другий приклад - це просто підказка щодо збереження інтерфейсу користувача, коли він у циклі або коли потрібно додати елементи до довгого списку.
Люк Пуплетт

І те, що ви зробили, зробило синхронний код асинхронним. Ви не підтримували інтерфейс, що відповідає інтерфейсу, в межах синхронного методу, як зазначено в описі, або відповідь просить.
Серві

0

Зробіть свій DoEvent () у WPF:

Thread t = new Thread(() => {
            // do some thing in thread
            
            for (var i = 0; i < 500; i++)
            {
                Thread.Sleep(10); // in thread

                // call owner thread
                this.Dispatcher.Invoke(() => {
                    MediaItem uc = new MediaItem();
                    wpnList.Children.Add(uc);
                });
            }
            

        });
        t.TrySetApartmentState(ApartmentState.STA); //for using Clipboard in Threading
        t.Start();

Працюй добре для мене!


-2

Відповідаючи на вихідне запитання: Де DoEvents?

Я думаю, що DoEvents - це VBA. І, схоже, VBA не має функції сну. Але VBA має спосіб отримати точно такий же ефект, як режим сну або затримки. Мені здається, що DoEvents еквівалентно режиму сну (0).

У VB та C # ви маєте справу з .NET. І оригінальне питання - це питання на C #. У C # ви б використовували Thread.Sleep (0), де 0 - 0 мілісекунд.

Тобі потрібно

using System.Threading.Task;

у верхній частині файлу, щоб використовувати

Sleep(100);

у вашому коді.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.