Globalnie łapać wyjątki w aplikacji WPF?

242

Mamy aplikację WPF, której części mogą generować wyjątki w czasie wykonywania. Chciałbym globalnie złapać każdy nieobsługiwany wyjątek i zapisać je, ale poza tym kontynuować wykonywanie programu tak, jakby nic się nie wydarzyło (trochę jak VB On Error Resume Next).

Czy jest to możliwe w C #? A jeśli tak, to gdzie dokładnie powinienem umieścić kod obsługi wyjątków?

Obecnie nie widzę żadnego punktu, w którym mógłbym zawinąć try/ catchdookoła i który wychwyciłby wszystkie możliwe wyjątki. I nawet wtedy zostawiłbym wszystko, co zostało stracone z powodu połowu. A może myślę tutaj w strasznie złych kierunkach?

ETA: Ponieważ wiele osób poniżej zwróciło na to uwagę: Aplikacja nie służy do kontrolowania elektrowni jądrowych. Jeśli się zawiesi, to nie jest to wielka sprawa, ale przypadkowe wyjątki, które są głównie związane z interfejsem użytkownika, są uciążliwe w kontekście, w którym zostaną użyte. Było ich kilka (i prawdopodobnie nadal), a ponieważ wykorzystuje ona architekturę wtyczek i może być rozszerzana przez innych (w tym przypadku także studentów), więc nie ma doświadczonych programistów, którzy byliby w stanie napisać całkowicie bezbłędny kod).

Jeśli chodzi o wyjątki, które zostaną złapane: rejestruję je w pliku dziennika, łącznie z kompletnym śledzeniem stosu. To był sedno tego ćwiczenia. Żeby po prostu przeciwstawić się tym, którzy zbyt dosłownie przyjęli moją analogię do OERN VB.

Wiem, że ślepe ignorowanie niektórych klas błędów jest niebezpieczne i może uszkodzić moją instancję aplikacji. Jak powiedziano wcześniej, ten program nie jest kluczowy dla nikogo. Nikt przy zdrowych zmysłach nie postawiłby na to przetrwanie ludzkiej cywilizacji. To po prostu małe narzędzie do testowania określonych podejść projektowych wrt. Inżynieria oprogramowania.

Do natychmiastowego użycia aplikacji nie ma wielu rzeczy, które mogą się zdarzyć na wyjątku:

  • Bez obsługi wyjątków - okno dialogowe błędu i wyjście z aplikacji. Eksperyment należy powtórzyć, choć prawdopodobnie z innym tematem. Żadne błędy nie zostały zarejestrowane, co jest niefortunne.
  • Ogólna obsługa wyjątków - łagodny błąd w pułapce, bez szkody. To powinien być powszechny przypadek oceniany na podstawie wszystkich błędów, które widzieliśmy podczas programowania. Ignorowanie tego rodzaju błędów nie powinno mieć bezpośrednich konsekwencji; podstawowe struktury danych są testowane wystarczająco dobrze, aby z łatwością to przetrwały.
  • Obsługa wyjątków ogólnych - poważny błąd w pułapce, prawdopodobnie awaria w późniejszym momencie. Może się to zdarzyć rzadko. Do tej pory nigdy tego nie widzieliśmy. Błąd i tak jest rejestrowany, a awaria może być nieunikniona. Jest to więc koncepcyjnie podobne do pierwszego przypadku. Tyle że mamy ślad stosu. W większości przypadków użytkownik nawet tego nie zauważy.

Jeśli chodzi o dane eksperymentu generowane przez program: poważny błąd w najgorszym przypadku po prostu nie spowodowałby zapisu danych. Subtelne zmiany, które zmieniają nieznacznie wynik eksperymentu, są raczej mało prawdopodobne. I nawet w takim przypadku, jeśli wyniki wydają się wątpliwe, błąd został zarejestrowany; nadal można wyrzucić ten punkt danych, jeśli jest on całkowitą wartością odstającą.

Podsumowując: Tak, uważam się za przynajmniej częściowo zdrowego rozsądku i nie uważam globalnej procedury obsługi wyjątków, która pozostawia działający program za koniecznie całkowicie zły. Jak już wspomniano dwa razy, taka decyzja może być ważna, w zależności od wniosku. W tym przypadku została uznana za ważną decyzję, a nie całkowitą i całkowitą bzdurę. W przypadku każdej innej aplikacji ta decyzja może wyglądać inaczej. Ale proszę nie oskarżaj mnie ani innych ludzi, którzy pracowali nad tym projektem, aby potencjalnie wysadzili świat tylko dlatego, że ignorujemy błędy.

Uwaga dodatkowa: dla tej aplikacji jest dokładnie jeden użytkownik. Nie jest to coś takiego jak Windows czy Office, z którego korzystają miliony, gdzie koszt bańki wyjątków dla użytkownika byłby już zupełnie inny.

Joey
źródło
To nie do końca myślenie - tak, prawdopodobnie chcesz, aby aplikacja została zamknięta. ALE, czy nie byłoby miło najpierw zarejestrować wyjątek za pomocą StackTrace? Jeśli wszystko, co otrzymałeś od użytkownika, brzmiało: „Twoja aplikacja uległa awarii, gdy nacisnąłem ten przycisk”, możesz nigdy nie być w stanie rozwiązać problemu, ponieważ nie posiadasz wystarczających informacji. Ale jeśli zarejestrowałeś wyjątek po raz pierwszy, zanim przyjemniej przerwiesz aplikację, będziesz mieć znacznie więcej informacji.
Russ
Rozbudowałem teraz nieco tę kwestię w pytaniu. Znam związane z tym ryzyko i dla tego konkretnego zastosowania zostało to uznane za dopuszczalne. I przerwanie aplikacji dla czegoś tak prostego jak indeks poza zakresem, podczas gdy interfejs użytkownika próbował zrobić fajną animację, jest przesadzone i niepotrzebne. Tak, nie znam dokładnej przyczyny, ale mamy dane na poparcie twierdzenia, że ​​zdecydowana większość przypadków błędów jest łagodna. Poważne, które maskujemy, mogą spowodować awarię aplikacji, ale tak by się stało bez globalnej obsługi wyjątków.
Joey,
Kolejna uwaga: jeśli dzięki temu podejściu zapobiegniesz awariom, użytkownicy najprawdopodobniej pokochają to.
lahjaton_j
Zobacz Windows Obsługa nieobsługiwanych wyjątków w WPF (najbardziej kompletny zbiór programów obsługi) w C # dla Visual Studio 2010 . Zawiera 5 przykładów, w tym AppDomain.CurrentDomain.FirstChanceException, Application.DispatcherUnhandledException i AppDomain.CurrentDomain.UnhandledException.
user34660,
Chciałbym dodać, że przepływ kodu podobny do VB On Error Resume Nextnie jest możliwy w języku C #. Po Exception(C # nie ma „błędów”) nie można po prostu wznowić z następną instrukcją: wykonywanie będzie kontynuowane w catchbloku - lub w jednej z procedur obsługi zdarzeń opisanych w odpowiedziach poniżej.
Mike

Odpowiedzi:

191

Użyj Application.DispatcherUnhandledException Event. Podsumuj to pytanie (patrz odpowiedź Drew Noakesa ).

Pamiętaj, że nadal będą wyjątki, które uniemożliwiają pomyślne wznowienie działania aplikacji, na przykład po przepełnieniu stosu, wyczerpaniu pamięci lub utracie połączenia z siecią podczas próby zapisania w bazie danych.

David Schmitt
źródło
Dzięki. I tak, zdaję sobie sprawę, że są wyjątki, z których nie mogę się zregenerować, ale zdecydowana większość z tych, które mogłyby się zdarzyć, jest w tym przypadku łagodna.
Joey,
14
Jeśli wyjątek występuje w wątku w tle (np. Przy użyciu ThreadPool.QueueUserWorkItem), to nie zadziała.
Szymon Rozga,
@siz: dobra uwaga, ale można sobie z tym poradzić za pomocą zwykłego bloku próbnego w środowisku roboczym, prawda?
David Schmitt
1
@PitiOngmongkolkul: Program obsługi jest wywoływany jako zdarzenie z głównej pętli użytkownika. Po powrocie procedur obsługi zdarzeń aplikacja działa normalnie.
David Schmitt,
4
Wydaje się, że musimy ustawić e.Handled = true, gdzie e jest DispatcherUnhandledExceptionEventArgs, aby pominąć domyślny moduł obsługi, który zamyka programy. msdn.microsoft.com/en-us/library/…
Piti Ongmongkolkul
55

Przykładowy kod wykorzystujący NLog, który będzie przechwytywał wyjątki zgłaszane ze wszystkich wątków w AppDomain , z wątku dyspozytora interfejsu użytkownika i funkcji asynchronicznych :

App.xaml.cs:

public partial class App : Application
{
    private static Logger _logger = LogManager.GetCurrentClassLogger();

    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);

        SetupExceptionHandling();
    }

    private void SetupExceptionHandling()
    {
        AppDomain.CurrentDomain.UnhandledException += (s, e) =>
            LogUnhandledException((Exception)e.ExceptionObject, "AppDomain.CurrentDomain.UnhandledException");

        DispatcherUnhandledException += (s, e) =>
        {
            LogUnhandledException(e.Exception, "Application.Current.DispatcherUnhandledException");
            e.Handled = true;
        };

        TaskScheduler.UnobservedTaskException += (s, e) =>
        {
            LogUnhandledException(e.Exception, "TaskScheduler.UnobservedTaskException");
            e.SetObserved();
        };
    }

    private void LogUnhandledException(Exception exception, string source)
    {
        string message = $"Unhandled exception ({source})";
        try
        {
            System.Reflection.AssemblyName assemblyName = System.Reflection.Assembly.GetExecutingAssembly().GetName();
            message = string.Format("Unhandled exception in {0} v{1}", assemblyName.Name, assemblyName.Version);
        }
        catch (Exception ex)
        {
            _logger.Error(ex, "Exception in LogUnhandledException");
        }
        finally
        {
            _logger.Error(exception, message);
        }
    }
Hüseyin Yağlı
źródło
10
To jest najbardziej kompletna odpowiedź tutaj! Uwzględniono wyjątki programu planującego Taks. Najlepszy dla mnie, czysty i prosty kod.
Nikola Jovic
3
Niedawno miałem uszkodzenie aplikacji App.config u klienta, a jej aplikacja nawet się nie uruchomiła, ponieważ NLog próbował odczytać z App.config i zgłosił wyjątek. Ponieważ ten wyjątek znajdował się w inicjalizatorze rejestratora statycznego , program nie przechwycił go UnhandledException. Musiałem spojrzeć na Windows Event Log Viewer, aby dowiedzieć się, co się dzieje ...
heltonbiker
Ja polecam do zestawu e.Handled = true;u UnhandledException, że aplikacja nie będzie katastrofy na Wyjątki UI
Apfelkuacha
30

AppDomain.UnhandledException Event

To wydarzenie zapewnia powiadomienie o niewyłapanych wyjątkach. Pozwala aplikacji na rejestrowanie informacji o wyjątku, zanim domyślna procedura obsługi systemu zgłosi użytkownikowi wyjątek i zakończy działanie aplikacji.

   public App()
   {
      AppDomain currentDomain = AppDomain.CurrentDomain;
      currentDomain.UnhandledException += new UnhandledExceptionEventHandler(MyHandler);    
   }

   static void MyHandler(object sender, UnhandledExceptionEventArgs args) 
   {
      Exception e = (Exception) args.ExceptionObject;
      Console.WriteLine("MyHandler caught : " + e.Message);
      Console.WriteLine("Runtime terminating: {0}", args.IsTerminating);
   }

Jeśli zdarzenie UnhandledException jest obsługiwane w domyślnej domenie aplikacji, jest tam zgłaszane dla każdego nieobsługiwanego wyjątku w dowolnym wątku, bez względu na domenę aplikacji, w której wątek się zaczął. Jeśli wątek został uruchomiony w domenie aplikacji, która ma moduł obsługi zdarzeń dla UnhandledException, zdarzenie jest wywoływane w tej domenie aplikacji. Jeśli ta domena aplikacji nie jest domyślną domeną aplikacji, a w domyślnej domenie aplikacji istnieje moduł obsługi zdarzeń, zdarzenie jest wywoływane w obu domenach aplikacji.

Załóżmy na przykład, że wątek zaczyna się w domenie aplikacji „AD1”, wywołuje metodę w domenie aplikacji „AD2”, a stamtąd wywołuje metodę w domenie aplikacji „AD3”, w której zgłasza wyjątek. Pierwszą domeną aplikacji, w której można wywołać zdarzenie UnhandledException, jest „AD1”. Jeśli ta domena aplikacji nie jest domyślną domeną aplikacji, zdarzenie można również wywołać w domyślnej domenie aplikacji.

CharithJ
źródło
kopiowanie z adresu URL, który wskazałeś (myślę tylko o aplikacjach konsolowych i aplikacjach WinForms, myślę): „Począwszy od .NET Framework 4, to zdarzenie nie jest zgłaszane z powodu wyjątków, które uszkadzają stan procesu, takich jak przepełnienie stosu lub naruszenia zasad dostępu, chyba że procedura obsługi zdarzeń ma krytyczne znaczenie dla bezpieczeństwa i ma atrybut HandleProcessCorruptStateExceptionsAttribute. ”
George Birbilis
1
@GeorgeBirbilis: Możesz subskrybować zdarzenie UnhandledException w konstruktorze aplikacji.
CharithJ
18

Oprócz tego, co wspomnieli tu inni, zauważ, że łączenie Application.DispatcherUnhandledException(i jego podobnych ) z

<configuration>
  <runtime>  
    <legacyUnhandledExceptionPolicy enabled="1" />
  </runtime>
</configuration>

w app.config będzie zapobiec wtórnej nici wyjątek od zamykania aplikacji.

Hertzel Guinness
źródło
2

Oto pełny przykład użycia NLog

using NLog;
using System;
using System.Windows;

namespace MyApp
{
    /// <summary>
    /// Interaction logic for App.xaml
    /// </summary>
    public partial class App : Application
    {
        private static Logger logger = LogManager.GetCurrentClassLogger();

        public App()
        {
            var currentDomain = AppDomain.CurrentDomain;
            currentDomain.UnhandledException += CurrentDomain_UnhandledException;
        }

        private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
        {
            var ex = (Exception)e.ExceptionObject;
            logger.Error("UnhandledException caught : " + ex.Message);
            logger.Error("UnhandledException StackTrace : " + ex.StackTrace);
            logger.Fatal("Runtime terminating: {0}", e.IsTerminating);
        }        
    }


}
Deweloper
źródło
-4

Jak „Wznów błędu na VB dalej?” To brzmi trochę przerażająco. Pierwsza rekomendacja to nie rób tego. Drugie zalecenie to nie rób tego i nie myśl o tym. Musisz lepiej odizolować swoje usterki. Sposób podejścia do tego problemu zależy od struktury kodu. Jeśli używasz wzoru takiego jak MVC lub podobny, nie powinno to być zbyt trudne i zdecydowanie nie wymagałoby globalnego połykania wyjątków. Po drugie, poszukaj dobrej biblioteki rejestrowania, takiej jak log4net lub użyj śledzenia. Musielibyśmy wiedzieć więcej szczegółów, takich jak o jakich wyjątkach mówisz i jakie części Twojej aplikacji mogą powodować zgłaszanie wyjątków.

BobbyShaftoe
źródło
2
Chodzi o to, że chcę, aby aplikacja działała nawet po nieprzechwyconych wyjątkach. Chcę je tylko zalogować (mamy do tego niestandardową strukturę rejestrowania, w zależności od naszych potrzeb) i nie muszę przerywać całego programu tylko dlatego, że niektóre wtyczki zrobiły coś dziwnego w kodzie interakcji użytkownika. Z tego, co do tej pory widziałem, nie jest trywialne izolowanie przyczyn, ponieważ różne części tak naprawdę się nie znają.
Joey,
9
Rozumiem, ale musisz być bardzo ostrożny, aby kontynuować po wyjątku, którego nie badasz, istnieje możliwość uszkodzenia danych i tak dalej.
BobbyShaftoe
3
Zgadzam się z BobbyShaftoe. To nie jest właściwa droga. Pozwól aplikacji ulec awarii. Jeśli błąd dotyczy jakiejś wtyczki, to napraw lub poproś kogoś o naprawienie wtyczki. Pozostawienie go uruchomionego jest BARDZO niebezpieczne. Dostaniesz dziwne efekty uboczne i dojdziesz do punktu, w którym nie będziesz w stanie znaleźć logicznego wyjaśnienia błędów występujących w twojej aplikacji.
1
Rozbudowałem teraz nieco tę kwestię w pytaniu. Znam związane z tym ryzyko i dla tego konkretnego zastosowania zostało to uznane za dopuszczalne. I przerwanie aplikacji dla czegoś tak prostego jak indeks poza zakresem, podczas gdy interfejs użytkownika próbował zrobić fajną animację, jest przesadzone i niepotrzebne. Tak, nie znam dokładnej przyczyny, ale mamy dane na poparcie twierdzenia, że ​​zdecydowana większość przypadków błędów jest łagodna. Poważne, które maskujemy, mogą spowodować awarię aplikacji, ale tak by się stało bez globalnej obsługi wyjątków.
Joey
To powinien być komentarz, a nie odpowiedź. Odpowiedź na „jak to zrobić” za pomocą „prawdopodobnie nie powinieneś” nie jest odpowiedzią, jest komentarzem.
Josh Noe