Gdy zadania asynchroniczne tworzą zły UX

9

Piszę dodatek COM, który rozszerza IDE, które desperacko tego potrzebuje. W grę wchodzi wiele funkcji, ale ograniczmy się do 2 ze względu na ten post:

  • Istnieje okno narzędzi Code Explorer, które wyświetla widok drzewa, który pozwala użytkownikowi nawigować po modułach i ich członkach.
  • Jest Kontrole Code toolwindow który wyświetla DataGridView , który pozwala poruszać się problemy kodu użytkownika i automatycznie je naprawić.

Oba narzędzia mają przycisk „Odśwież”, który uruchamia asynchroniczne zadanie, które analizuje cały kod we wszystkich otwartych projektach; Code Explorer używa analizowania wyników zbudować katalogów , a kod Kontrole wykorzystuje analizowania wyników, aby znaleźć problemy kodu i wyświetla wyniki w swojej DataGridView .

To, co próbuję tutaj zrobić, to udostępnienie wyników analizy między funkcjami, aby po odświeżeniu Code Explorera , Inspekcje kodu wiedziały o tym i mogły się odświeżyć bez konieczności ponownego wykonywania analizy składniowej, którą właśnie wykonał Code Explorer .

Więc co zrobiłem, uczyniłem moją klasę analizatora składni zdarzeniem, do którego funkcje mogą się zarejestrować:

    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);
            }
        });
    }

I to działa. Problem, który mam, polega na tym, że ... działa - to znaczy, kiedy inspekcje kodu są odświeżane, parser mówi eksploratorowi kodu (i wszystkim innym) "koleś, czyjaś analiza, czy chcesz coś z tym zrobić? „ - a kiedy parsowanie się zakończy, parser mówi swoim słuchaczom: „chłopaki, mam dla ciebie świeże wyniki parsowania, cokolwiek chcesz z tym zrobić?”.

Pozwól, że przedstawię ci przykład ilustrujący problem, który to stwarza:

  • Użytkownik wywołuje Eksploratora kodu, który mówi użytkownikowi „poczekaj, pracuję tutaj”; użytkownik kontynuuje pracę w IDE, Code Explorer przerysowuje się, życie jest piękne.
  • Następnie użytkownik wywołuje Inspekcje kodu, które mówią użytkownikowi „poczekaj, pracuję tutaj”; parser mówi Eksploratorowi kodu „koleś, czyjaś analiza, czy chcesz coś z tym zrobić?” - Code Explorer mówi użytkownikowi „poczekaj, pracuję tutaj”; użytkownik nadal może pracować w środowisku IDE, ale nie może poruszać się po Eksploratorze kodu, ponieważ jest odświeżany. I czeka też na zakończenie kontroli kodu.
  • Użytkownik widzi problem z kodem w wynikach kontroli, którą chce rozwiązać; kliknij dwukrotnie, aby przejść do niego, potwierdź problem z kodem i kliknij przycisk „Napraw”. Moduł został zmodyfikowany i musi zostać ponownie przeanalizowany, aby umożliwić kontynuację inspekcji kodu; Code Explorer mówi użytkownikowi „poczekaj, pracuję tutaj”, ...

Widzisz dokąd to zmierza? Nie podoba mi się to i założę się, że użytkownikom też się nie spodoba. czego mi brakuje? Jak powinienem udostępniać wyniki analizy między funkcjami, ale nadal pozostawić kontrolę nad tym, kiedy funkcja powinna działać ?

Powodem, dla którego pytam, jest to, że pomyślałem, że jeśli odłożę rzeczywistą pracę, aż użytkownik aktywnie zdecyduje się odświeżyć, i „buforuję” wyniki analizy, gdy się pojawią… cóż, odświeżyłbym widok drzewa i lokalizowanie problemów z kodem w prawdopodobnie przestarzałym wyniku analizy ... co dosłownie sprowadza mnie z powrotem do punktu wyjścia, gdzie każda funkcja działa z własnymi wynikami analizy: czy jest jakiś sposób, aby udostępnić wyniki analizy między funkcjami i mieć piękny UX?

Kod to , ale nie szukam kodu, szukam pojęć .

Mathieu Guindon
źródło
2
Tylko dla Ciebie, mamy również stronę UserExperience.SE . Wydaje mi się, że jest to temat na ten temat, ponieważ omawia projektowanie kodu bardziej niż interfejs użytkownika, ale chciałem poinformować cię na wypadek, gdyby Twoje zmiany przesunęły się bardziej w stronę interfejsu użytkownika, a nie w stronę kodu / projektu problemu.
Czy podczas analizowania jest to operacja „wszystko albo nic”? Na przykład: czy zmiana w pliku wyzwala pełną ponowną analizę, czy tylko dla tego pliku i tych, które od niego zależą?
Morgen
@Morgen są dwie rzeczy: VBAParserjest generowany przez ANTLR i daje mi parsowanie, ale funkcje tego nie zużywają. RubberduckParserBierze drzewo składniowy, spacery, i zgłosi VBProjectParseResult, który zawiera Declarationobiekty, które posiadają wszystkie ich Referencesrozwiązany - to jakie cechy biorą na wejściu .. tak tak, to dość dużo sytuacja wszystko albo nic. RubberduckParserJest wystarczająco inteligentny, aby nie re-parse modułów, które nie zostały zmodyfikowane chociaż. Ale jeśli istnieje wąskie gardło, nie dotyczy to analizowania, lecz kontroli kodu.
Mathieu Guindon
4
Myślę, że zrobiłbym to w ten sposób: kiedy użytkownik uruchamia odświeżanie, to okno narzędzi uruchamia analizę i pokazuje, że działa. Inne okna narzędzi nie są jeszcze powiadamiane, nadal wyświetlają stare informacje. Aż do zakończenia parsera. W tym momencie analizator składni zasygnalizuje wszystkim oknom narzędzi, aby odświeżyły swój widok nowymi informacjami. Jeśli użytkownik przejdzie do innego okna narzędzi podczas pracy analizatora składni, okno to również przejdzie w stan „pracujący ...” i zasygnalizuje ponowną próbę. Analizator składni zacząłby wtedy od nowa dostarczać aktualne informacje do wszystkich okien jednocześnie.
cmaster
2
@cmaster Chciałbym również głosować nad tym komentarzem jako odpowiedzią.
RubberDuck

Odpowiedzi:

7

Sposób, w jaki prawdopodobnie do tego podchodziłbym, polegałby na mniejszym skupieniu się na zapewnianiu doskonałych rezultatów, a zamiast tego na podejściu opartym na najlepszym wysiłku. Spowodowałoby to co najmniej następujące zmiany:

  • Przekształć logikę, która obecnie rozpoczyna ponowną analizę, aby zażądać zamiast zainicjować.

    Logika żądania ponownego przeanalizowania może wyglądać mniej więcej tak:

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

    Zostanie to połączone z logiką opakowującą parser, która może wyglądać mniej więcej tak:

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

    Ważną rzeczą jest to, że analizator składni działa, dopóki nie zostanie spełnione ostatnie żądanie ponownej analizy, ale w danym momencie nie działa więcej niż jeden analizator składni.

  • Usuń połączenie ParseStartedzwrotne. Żądanie ponownej analizy jest teraz operacją typu „zapomnij”.

    Ewentualnie przekonwertuj go, aby nie robił nic poza pokazywaniem wskaźnika odświeżania w części drogi GUI, która nie blokuje interakcji użytkownika.

  • Spróbuj zapewnić minimalną obsługę nieaktualnych wyników.

    W przypadku Eksploratora kodu może to być tak proste, jak szukanie rozsądnej liczby wierszy w górę iw dół dla metody, do której użytkownik chce nawigować, lub najbliższej metody, jeśli nie znaleziono dokładnej nazwy.

    Nie jestem pewien, co byłoby odpowiednie dla Inspektora kodu.

Nie jestem pewien szczegółów implementacji, ale ogólnie rzecz biorąc, jest to bardzo podobne do tego, jak edytor NetBeans obsługuje to zachowanie. Zawsze bardzo szybko można zauważyć, że obecnie jest ono odświeżane, ale również nie blokuje dostępu do funkcji.

Stale wyniki są często wystarczająco dobre - zwłaszcza w porównaniu z brakiem wyników.

Morgen
źródło
1
Doskonałe punkty, ale mam pytanie: używam ParseStarteddo wyłączenia przycisku [Odśwież] ( Control.EnableRefresh(false)). Jeśli usunę to wywołanie zwrotne i pozwolę użytkownikowi je kliknąć ... wtedy postawiłbym się w sytuacji, w której mam dwa równoległe zadania, które parsują ... jak tego uniknąć, nie wyłączając odświeżania wszystkich innych funkcji, gdy ktoś parsuje?
Mathieu Guindon
@ Mat'sMug Zaktualizowałem moją odpowiedź, aby uwzględnić ten aspekt problemu.
Morgen,
Zgadzam się z tym podejściem, z tym wyjątkiem, że nadal utrzymuję ParseStartedzdarzenie, na wypadek gdybyś chciał pozwolić interfejsowi użytkownika (lub innemu składnikowi) czasami ostrzegać użytkownika o ponownej próbie. Oczywiście możesz udokumentować, że osoby dzwoniące powinny próbować nie powstrzymywać użytkownika od używania (prawdopodobnie być) nieaktualnych bieżących wyników analizy.
Mark Hurd