Коли асинхронні завдання роблять поганий UX


9

Я пишу COM-надбудову, яка розширює IDE, яка відчайдушно потребує цього. Є багато функцій, але давайте звузимо їх до 2 заради цієї публікації:

  • Існує вікно інструментів Code Explorer, яке відображає вигляд дерева, що дозволяє користувачеві переміщатися по модулях та їх членам.
  • Там в Інспектування коду ToolWindow , який відображає DataGridView , який дозволяє проблеми користувачеві переміщатися код і автоматично виправити їх.

В обох інструментах є кнопка «Оновити», яка запускає асинхронну задачу, яка аналізує весь код у всіх відкритих проектах; Code Explorer використовує результати синтаксичного аналізу для побудови TreeView , а код Огляди використовує синтаксичнийаналіз результати пошуку проблем коди і відображення результатів у своїй DataGridView .

Що я намагаюся зробити тут, це поділити результати аналізу між функціями, так що коли Code Explorer оновлюється, то Кодекс перевірки знає про це і може оновитись без необхідності повторювати роботу розбору, яку щойно робив Провідник коду .

Отже, що я зробив, я зробив свій аналізатор класу провайдером подій, до яких функції можуть зареєструватися:

    private void _parser_ParseCompleted(object sender, ParseCompletedEventArgs e)
    {
        Control.Invoke((MethodInvoker) delegate
        {
            Control.SolutionTree.Nodes.Clear();
            foreach (var result in e.ParseResults)
            {
                var node = new TreeNode(result.Project.Name);
                node.ImageKey = "Hourglass";
                node.SelectedImageKey = node.ImageKey;

                AddProjectNodes(result, node);
                Control.SolutionTree.Nodes.Add(node);
            }
            Control.EnableRefresh();
        });
    }

    private void _parser_ParseStarted(object sender, ParseStartedEventArgs e)
    {
        Control.Invoke((MethodInvoker) delegate
        {
            Control.EnableRefresh(false);
            Control.SolutionTree.Nodes.Clear();
            foreach (var name in e.ProjectNames)
            {
                var node = new TreeNode(name + " (parsing...)");
                node.ImageKey = "Hourglass";
                node.SelectedImageKey = node.ImageKey;

                Control.SolutionTree.Nodes.Add(node);
            }
        });
    }

І це працює. Проблема, яка у мене виникає, полягає в тому, що ... це працює - я маю на увазі, коли перевірки коду будуть оновлені, аналізатор повідомляє провідникові коду (і всім іншим) "чувак, чийсь синтаксичний розбір, все, що ти хочеш з цим робити?" " - і коли синтаксичний розбір завершується, аналізатор повідомляє своїм слухачам "хлопці, у мене є для вас свіжі результати розбору, все, що ви хочете з цим зробити?".

Дозвольте ознайомити вас із прикладом, щоб проілюструвати проблему, яку це створює:

  • Користувач виводить Провідник коду, який говорить користувачеві "тримайтесь, я працюю тут"; Користувач продовжує працювати в IDE, Code Explorer переробляє себе, життя прекрасне.
  • Потім користувач відкриває інспекції коду, які повідомляють користувачеві "затримайтеся, я працюю тут"; аналізатор каже Провідник коду: "чувак, чийсь розбір, все, що ти хочеш з цим зробити?" - Провідник коду повідомляє користувачеві "тримайтесь, я працюю тут"; Користувач все ще може працювати в IDE, але не може переміщатися в Провіднику кодів, оскільки він оновлює. І він також чекає завершення перевірок коду.
  • Користувач бачить проблему з кодом у результатах перевірки, до яких хоче звернутися; вони двічі клацнуть, щоб перейти до нього, підтвердити наявність проблеми з кодом та натиснути кнопку "Виправити". Модуль був модифікований і його потрібно переаналізувати, тому перевірки коду тривають з ним; Провідник коду каже користувачеві "тримайтесь, я працюю тут", ...

Бачите, куди це йде? Мені це не подобається, і сподіваюся, що і користувачам це не сподобається. Що я пропускаю? Як мені слід ділитися результатами розбору між функціями, але все ж залишати користувачеві контроль, коли функція повинна виконувати свою роботу ?

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

Код є , але я не шукаю коду, я шукаю поняття .


2
Просто FYI, у нас також є сайт UserExperience.SE . Я вважаю, що це тематично тут, тому що він обговорює дизайн коду більше, ніж інтерфейс користувача, але я хотів би повідомити вас про те, якщо ваші зміни більше спрямовуються на сторону інтерфейсу, а не на кодову / дизайнерську сторону проблеми.

Коли ви розбираєтеся, чи це операція «все чи ні»? Наприклад: чи зміна файлу викликає повний перегляд чи тільки для цього файлу та тих, що від нього залежать?
Морген

@Morgen є дві речі: VBAParserгенерується ANTLR і дає мені розбір дерева, але функції цього не використовують. RubberduckParserПриймає дерево розбору, прогулянки, і видає , VBProjectParseResultщо містить Declarationоб'єкти , які мають всі їх Referencesвирішити - що - х , які функції приймають на вхід .. так що так, це в значній мірі все-або-нічого ситуація. Це RubberduckParserдосить розумно, щоб не переаналізувати модулі, які не були змінені. Але якщо є вузьке вузьке місце, це не те, що він розбирає, це перевірка коду.
Матьє Гіндон

4
Я думаю, я би зробив це так: Коли користувач запускає оновлення, це вікно інструментів запускає аналіз і показує, що він працює. Про інші вікна інструментів ще не повідомлено, вони постійно відображають стару інформацію. Поки парсер не закінчиться. У цей момент аналізатор буде сигналізувати всі вікна інструментів, щоб оновити свій погляд новою інформацією. Якщо користувач перейде до іншого вікна інструментів, коли аналізатор працює, це вікно також увійде в стан "працює ..." і подасть сигнал про повторний аналіз. Потім аналізатор почне спочатку доставляти актуальну інформацію у всі вікна.
cmaster - відновити моніку

2
@cmaster Я б підтримав цей коментар і як відповідь.
RubberDuck

Відповіді:


7

Я б, напевно, підходив до цього, полягав у тому, щоб менше зосереджуватись на забезпеченні ідеальних результатів, а замість цього зосередитись на підході з найкращих зусиль. Це призведе до принаймні таких змін:

  • Перетворіть логіку, яка наразі запускає повторний аналіз для запиту, а не ініціювання.

    Логіка запиту на повторний аналіз може виглядати приблизно так:

    IF parseIsRunning IS false
      startParsingThread()
    ELSE
      SET shouldParse TO true
    END
    

    Це буде в поєднанні з логікою обгортання аналізатора, який може виглядати приблизно так:

    SET parseIsRunning TO true
    DO 
      SET shouldParse TO false
      doParsing()
    WHILE shouldParse IS true
    SET parseIsRunning TO false
    

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

  • Видаліть ParseStartedзворотний дзвінок. Запит на повторний аналіз зараз - це пожежа та забудьте операцію.

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

  • Постарайтеся забезпечити мінімальну обробку для несвіжих результатів.

    У випадку Провідника коду це може бути таким же простим, як пошук розумної кількості рядків вгору і вниз для методу, до якого користувач хоче перейти, або найближчого методу, якщо точне ім’я не знайдено.

    Я не впевнений, що було б доречно інспектору кодексу.

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

Застійні результати часто є досить хорошими - особливо в порівнянні з відсутніми результатами.


1
Чудові бали, але у мене є питання: я використовую ParseStartedдля відключення кнопки [Refresh] ( Control.EnableRefresh(false)). Якщо я видалю цей зворотний виклик і дозволю користувачу натиснути його ... тоді я поставив би себе в ситуацію, коли у мене є два одночасні завдання, які роблять аналіз ... як я уникаю цього, не вимикаючи оновлення всіх інших функцій, коли хтось розбирає?
Матьє Гіндон

@ Mat'sMug Я оновив свою відповідь, щоб включити цю грань проблеми.
Морген

Я погоджуюся з таким підходом, за винятком того, що я б все-таки дотримувався ParseStarted подію, якщо ви хочете дозволити користувальницькому інтерфейсу (або іншому компоненту) іноді попереджати користувача про повторний аналіз. Звичайно, ви можете задокументувати, що абоненти повинні намагатися не зупинити користувача на використанні (невдовзі) застарілих поточних результатів розбору.
Марк Херд
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.