Окно сообщения об ошибке для WinForms и WPF приложений

  • Tutorial

Приветствую!

В статье посвященной моему профайлеру для Entity Framework-a, я вкратце описал примененную мной форму для сообщения пользователю об исключительной ошибке в приложении. После оценки количества скачиваний примера кода, было решено выделить этот пример в отдельный проект, а также добавить поддержку WPF приложений.
Исходники библиотеки вместе с примерами опубликованы на CodePlex под свободной лицензией MIT: https://uiexceptionhandler.codeplex.com/

Подробности под катом.

Введение

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

Что получилось

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


При клике по кнопке «Error detail information» выводиться дополнительная информация об ошибке:


Кнопка Debug позволяет подключить отладчик Visual Studio.
Кнопка «Send to Developer» отправляет письмо на почту разработчику. В случае ошибки отправки сообщения, пользователю будет предложено самому отправить лог файл разработчику на почту.
Отправленное разработчику сообщение придет в таком виде:


Использование

1. Забрать последнюю версию кода https://uiexceptionhandler.codeplex.com/SourceControl/latest
2. Собрать в Release mode.
3. Из папки «UIExceptionHandlerLibs\Deploy» подключить в проект библиотеку UIExceptionHandlerWinForms.dll в случае WinForms приложения и UIExceptionHandlerWPF.dll в случае WPF приложения.
4. Инициализировать путем вызова статического метода с рядом параметров:
UIException.Start(
   string serverSmtp, 
   int portSmtp, 
   string passwdSmtp, 
   string userSmtp, 
   string programmerEmail,
   string fromEmail, 
   string subject
)

Как это работает

Статический метод UIException.Start подписывает метод HandleError на событие AppDomain.CurrentDomain.UnhandledException:
AppDomain.CurrentDomain.UnhandledException += (sender, e) => HandleError((Exception)e.ExceptionObject);

Метод HandleError:
private static void HandleError(Exception exception)
{
    try
    {
        // запускаем обработчик формы и передаем ему ссылку на форму наследованную от интерфейса IErrorHandlerForm
        new ErrorHandlerController(exception, new ErrorHandlerForm()).Run();
    }
    catch (Exception e)
    {
        // сохраняем ошибку в лог файл
        LogHelper.Logger.Error(e);
        // в случае ошибки обработки выводим сообщение с просьбой отправить лог файл разработчику на почту
       MessageBox.Show("Error processing exception. Please send log file " + LogHelper.ExceptionLogFileName + " to developer: " + Settings.ProgrammerEmail + " \r\n Exception:" + e);
        // спрашиваем нужно ли подключить отладчик
        if (MessageBox.Show("Attach debugger? \n Only for developer!!!", "Debugging...", MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes)
        {
            Debugger.Launch();
            throw;
        }
    }
    finally
    {
        // обязательно завершаем приложение чтобы windows не вывела стандартное сообщение об ошибке
        Environment.Exit(1);
    }
}

Интерфейс IErrorHandlerForm:
public interface IErrorHandlerForm
{
    event Action OnSendButtonClick;
    event Action OnShowErrorLinkClick;
    event Action OnLogFileLinkClick;
    event Action OnDebugButtonClick;

    // меняет высоту формы
    void SetHeight(int height);
    // задает подробное сообщение об ошибке
    string ExceptionInfoText { get; set; }
    // получает текст из поля дополнительной информации введенной пользователем
    string ExceptionDetailText { get; set; }
    // email пользователя для ответа
    string ReplyEmail { get; }
    void ShowExceptionInfoTextBox(bool isShow);
    // выводит информационное сообщение
    void ShowInfoMessageBox( string text, string caption);
    // выводит диалоговое сообщение
    bool ShowQuestionDialog( string text, string caption);
    // показывает окно в режиме диалога! необходимо чтобы приложение дожидалось закрытия окна и завершилось в finaly
    void ShowViewDialog();
    void UpdateContactEmail(string contactEmail);
}

В качестве библиотеки для логгирования используется NLog. Для того чтобы избежать появления лишних xml файлов, вся конфигурация Nlog-а делается в коде:
private static void ConfigureNlog()
{
    var config = new LoggingConfiguration();

    var fileTarget = new FileTarget();
    config.AddTarget("file", fileTarget);

    fileTarget.Layout = @"${longdate} ${message}";
    fileTarget.FileName = "${basedir}/" + ExceptionLogFileName;

    var rule2 = new LoggingRule("*", LogLevel.Trace, fileTarget);
    config.LoggingRules.Add(rule2);

    LogManager.Configuration = config;
}

Чтобы добиться максимальной простой интеграции в проект, я решил все используемые сборки объединить в одну библиотеку. Делается это при помощи приложения ILMerge, путем добавления скрипта в post-build событие:
if $(ConfigurationName) == Release (
"$(SolutionDir)ILMerge\ILMerge.exe" /out:"$(SolutionDir)Deploy\$(TargetFileName)" "$(TargetDir)*.dll" /target:dll /targetplatform:v4,C:\Windows\Microsoft.NET\Framework64\v4.0.30319 /wildcards
)



Послесловие

Данное решение было написано для достаточно крупного проекта, применяется уже более 2-х лет, значительно улучшив процесс исправления ошибок, поскольку о каждом падении приложения узнаешь моментально, без дополнительной нотификации от пользователя.

Надеюсь это все будет кому-то полезно!
Всем спасибо за внимание!
Поделиться публикацией
Похожие публикации
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 34
  • +4
    Семь параметров в методе — у вас рак параметров.

    Более того — параметров в общем случае недостаточно, потому что нет настроек прокси и шифрования, а многие почтовые сервера работают с ними.

    Я бы рекомендовал (раз уж у нас Winforms/WPF) использовать Outlook Automation, и если он отсутствует — только тогда идти через отправку напрямую по SMTP.

    А параметры упаковать в класс — его будет проще расширить, когда параметров станет 20.
    • +2
      А если я использую другой почтовый клиент при установленном Outlook?
      • –4
        Ну, я могу вам посочувствовать.
    • 0
      >А параметры упаковать в класс — его будет проще расширить, когда параметров станет 20.
      Поясните пожалуйста подробнее что вы предлагаете, так как в любом случае необходимо инициализировать все параметры, в вариант конструктора ничем по сути не отличается. Спасибо.

      >Более того — параметров в общем случае недостаточно, потому что нет настроек прокси и шифрования, а многие почтовые сервера работают с ними.
      О таком кейсе я не подумал, так как в моем случае все обошлось простейшим SMTP, но благо весь код открытый, соответственно никто не мешает доделать под соответственные нужды.
      • +4
        Метод, у которого больше четырех параметров выглядит грязно. Только и всего. И читать его сложнее.
        • +2
          Соглашусь, но ничего лучше не придумал.
          • 0
            del, уже посоветовали
        • +3
          > Поясните пожалуйста подробнее что вы предлагаете, так как в любом случае необходимо инициализировать все параметры, в вариант конструктора ничем по сути не отличается. Спасибо.
          Ваш метод
          public static class UIException
              {
                  public static void Start(string serverSmtp, int portSmtp, string passwdSmtp, string userSmtp, string programmerEmail, string fromEmail, string exceptionSubject)
                  {
                      Settings.ServerSMTP = serverSmtp;
                      Settings.PortSMTP = portSmtp;
                      Settings.PasswdSMTP = passwdSmtp;
                      Settings.UserSMTP = userSmtp;
                      Settings.ProgrammerEmail = programmerEmail;
                      Settings.FromEmail = fromEmail;
                      Settings.ExceptionSubject = exceptionSubject;
          
          

          Можно сделать так:
          public static class UIException
              {
                  public static void Start(Settings settings)
                  {        
          
          

          Тогда вызов метода будет выглядеть как:
          UIExceptionHandlerWPF
                 .UIException
                 .Start(new Settings {
                          ServerSMTP: "SmtpServer",
                          PortSMTP:  26,
                          PasswdSMTP: "Password",
                          UserSMTP: "User",
                          ProgrammerEmai: "developer@gmail.com",
                          FromEmail: "user@gmail.com",
                          ExceptionSubject:  "Exception"
                        });
          
          • +3
            Все свойства должны быть инициализированы и случае использования вашего варианта придется делать дополнительную проверку на неинициализированные свойства, что в условиях подключаемой библиотеки срабатывающей только в случае исключительной ситуации рискованно.
            Как я отметил выше, вариант с конструктором по сути ничем не отличается.
            • +6
              В таком случае, как минимум, необходима проверка на правильность переданных данных, что логичней сделать internal методом IsValid в классе Settings, где и будет находиться вся логика проверки на инициализированность и правильность:

              public static class UIException
                  {
                      public static void Start(Settings settings)
                      {  
                          if(!settings.IsValid())
                             throw new Exception('Settings is not valid!');
              
              
              • +4
                Соглашусь, можно было сделать так.
                • 0
                  Вариант неплох, но проблема, что в данном случае библиотека, имеющая ошибку на этапе компиляции, будет выбрасывать исключение в рантайме. Да и вообще, библиотека обработки ошибок, бросающая исключения, это немного странно. Вариант ниже обладает теми же достоинствами, но исключает эти недостатки — создать объект можно только со всеми параметрами, а затем менять их можно только на валидные.
                  • 0
                    Получилось достаточно интересное Code Review!
                    Также, я бы хотел предложить всем желающим поучаствовать в развитие проекта написать мне в личку или на почту.
                    Предоставлю права на commit.
                    Спасибо.
                  • 0
                    Можно сделать конструктор с обязательными параметрами, а свойства сделать либо read-only, либо меняющие свое значение только при валидных данных (то есть не авто-свойства, а обычные свойства с какой-то логикой). Тогда settings будет всегда в валидном состоянии (такой вот транзакционный подход).
                • 0
                  Про то, что параметры следует выделить в отдельный класс согласен.
                  Не обязательно все параметры выставлять в конструкторе, можно сдлетаь объект с Fluent интерфейсом, например. И это не единственный способ.
                • 0
                  наверное имеет смысл «сообщение с просьбой отправить лог файл разработчику на почту» показывать после «сохраняем ошибку в лог файл»
                  • 0
                    Исправил, спасибо!
                  • 0
                    Вам совет, и не только вам. Лучше не используйте ILMerge, если не хотите проблем.
                    • +1
                      Использую уже во втором проекте, каких-либо проблем не испытываю.
                      Не могли вы поподробнее рассказать с какими проблемами можно столкнуться?
                      Спасибо.
                      • +1
                        Все очень просто. Как работает ILMerge: он читает полностью весь IL код в память, а потом пишет в отдельную сборку. Код, который использует IlMerge, написан в MS Research и не имеет ничего общего с компилятором. То, что на данном этапе все работает для вас — это хорошо, но я бы не стал на ILMerge сильно надеяться и использовать его везде, где только можно.
                        Основные проблемы начинаются там, где присутствует Type Forwarding. Один из примеров — Portable Class Libraries. А так же Библиотеки из разных версий .NET Framework 3.5 -> 4.0 -> 4.5 и т.п.
                        • +1
                          Спасибо за разъяснение!
                    • 0
                      Большая просьба тем кто проставил минусы статье и мне прокомментировать что именно не понравилось, так как судя по добавлениям в избранное статья все-таки кому-то полезна. Спасибо.
                      Также хочется отметить, что я стараюсь расшарить свои наработки с сообществом, теми решениями которые могут полезны, с той целью чтобы мои труды кому-то помогли как не раз чьи-то статьи выручали меня.
                      • 0
                        Я не уверена, но кажется статья слабенькая, по этому и минусуют.
                        Много сделано как «так делать нельзя», если вы меня понимаете.

                        А вообще, посмотрите в сторону WER (Windows Error Reporting).
                        • 0
                          Спасибо за комментария, правда я не понял почему статья слабенькая.
                          Это узкоспециализированная статья о моем бесплатном open source проекте, рассчитанная на .NET разработчиков.
                          В статье присутствует описание проекта со скриншотами, инструкция по использованию и объяснение как это работает.
                          Хотя конечно соглашусь, сложно сравнивать узкоспециализированную статью, от который я в большинстве своем получаю только минусы, с популярными на хабре обсуждениями политики, космонавтики, новых гаджетов и прочими популярными темами.
                          • +2
                            > Спасибо за комментария, правда я не понял почему статья слабенькая.
                            Не уровень Голливуда, видимо.
                            • 0
                              Стараюсь как могу.
                              • +1
                                Не обижайтесь, это была отсылка к нику выше :).
                      • 0
                        Знакомая задача. У нас похожая форма, только приложения одновременно и WinForms и WPF, причём какие-то проекты это WinForms хостящий WPF, а какие-то WPF хостящий WinForms. Пришлось навешиваться на обе темы одновременно, а окошко с уведомлением об ошибке делать на WPF.

                        Да, при обработке исключений мы ещё проверяем, что к проекту уже приаттачен отладчик и в этом случае окошко не открываем — от отладчика разработчик узнает гораздо больше, чем из пользовательского окошка.
                        • 0
                          Забавно, но я делаю точно также как и вы, это можно увидеть в моей прошлой статье.
                          if (!Debugger.IsAttached)    { AppDomain.CurrentDomain.UnhandledException += (sender, e) => HandleError((Exception)e.ExceptionObject);}
                          

                          Но в данном случае, по какой-то причине, в случае ошибки в приложении окно ошибки не показывается, пока еще не разбирался почему.
                          • +1
                            Нене, навешивать обработчик на необработанные исключения нужно в любом случае, а вот при обработке исключения уже стоит проверять на наличие отладчика. Отладчик может подключиться и отключиться в любой момент.
                        • 0
                          делал аналогичную формочку, но мы до кучи еще скриншот делали, иногда стек-трейса недостаточно.
                          • 0
                            Отличная идея, надо будет поиграться с этим.
                            • 0
                              От проекта зависит, но на скриншот может попасть персональная информация чему пользователь скорее всего будет не рад.
                              • 0
                                ну если в проекте все так серьезно, можно сделать галочку или кнопочку которая скриншот удалять будет.
                                еще полезно бывает логи приложения (не только стек) и app.config прилепить.

                            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.