Когда асинхронные задачи делают плохой UX

9

Я пишу надстройку COM, которая расширяет IDE, в которой она остро нуждается. Здесь задействовано много функций, но давайте сузим их до 2 ради этого поста:

  • Существует окно инструментов Code Explorer, которое отображает древовидное представление, которое позволяет пользователю перемещаться по модулям и их членам.
  • Существует окно инструментов проверки кода, которое отображает сетку данных, которая позволяет пользователю перемещаться по проблемам кода и автоматически исправлять их.

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

Здесь я пытаюсь поделиться результатами разбора между функциями, чтобы при обновлении Code Explorer Инспекции кода знали об этом и могли обновляться без необходимости повторения работы синтаксического анализа, которую только что сделал 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);
            }
        });
    }

И это работает. У меня проблема в том, что ... это работает - я имею в виду, когда проверки кода обновляются, анализатор говорит проводнику кода (и всем остальным): "Чувак, кто-то разбирает, что ты хочешь с этим делать?" " - и когда анализ завершается, парсер говорит слушателям: «Ребята, у меня есть свежие результаты разбора для вас, что вы хотите с этим сделать?».

Позвольте мне показать вам пример, чтобы проиллюстрировать эту проблему:

  • Пользователь вызывает Code Explorer, который говорит пользователю: «Держись, я здесь работаю»; Пользователь продолжает работать в IDE, Code Explorer перерисовывает себя, жизнь прекрасна.
  • Затем пользователь вызывает проверки кода, которые говорят пользователю «держись, я здесь работаю»; синтаксический анализатор говорит проводнику кода: "Чувак, кто-то разбирает, что ты хочешь с этим сделать?" - Code Explorer говорит пользователю «держись, я здесь работаю»; Пользователь все еще может работать в IDE, но не может перемещаться по Code Explorer, потому что он обновляется. И он ждет завершения проверки кода.
  • Пользователь видит проблему с кодом в результатах проверки, которую он хочет устранить; они дважды щелкают, чтобы перейти к нему, подтверждают, что есть проблема с кодом, и нажимают кнопку «Исправить». Модуль был изменен и требует повторного анализа, поэтому проверки кода продолжаются; Обозреватель кода говорит пользователю «держись, я здесь работаю», ...

Видишь, куда это идет? Мне это не нравится, и я уверен, что пользователям это тоже не понравится. Что мне не хватает? Как мне лучше обмениваться результатами анализа между функциями, но при этом оставить пользователю контроль над тем, когда функция должна выполнять свою работу ?

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

Код , но я не ищу код, я ищу концепции .

Матье Гиндон
источник
2
Просто для справки, у нас также есть сайт 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обратный вызов. Запрос на повторный анализ - теперь операция «забыл и забыл».

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

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

    В случае Code Explorer это может быть так же просто, как поиск разумного количества строк вверх и вниз для метода, к которому пользователь хочет перейти, или ближайшего метода, если точное имя не найдено.

    Я не уверен, что подойдет для инспектора кода.

Я не уверен в деталях реализации, но в целом это очень похоже на то, как редактор NetBeans обрабатывает это поведение. Это всегда очень быстро, чтобы указать, что это в настоящее время обновляет, но также не блокирует доступ к функциональности.

Устаревшие результаты часто достаточно хороши - особенно если сравнивать с отсутствием результатов.

Morgen
источник
1
Отличные очки, но у меня есть вопрос: я использую, ParseStartedчтобы отключить кнопку [Обновить] ( Control.EnableRefresh(false)). Если я удалю этот обратный вызов и позволю пользователю щелкнуть его ... тогда я попаду в ситуацию, когда у меня есть две параллельные задачи, выполняющие синтаксический анализ ... как избежать этого, не отключая обновление всех других функций, пока кто-то это разбор?
Матье Гиндон
@ Mat'sMug Я обновил свой ответ, чтобы включить этот аспект проблемы.
Морген
Я согласен с этим подходом, за исключением того, что я все равно сохраню ParseStartedсобытие, если вы хотите разрешить пользовательскому интерфейсу (или другому компоненту) иногда предупреждать пользователя о повторном выполнении. Конечно, вы можете захотеть задокументировать, что вызывающие абоненты должны пытаться не мешать пользователю использовать устаревшие результаты анализа.
Марк Херд