diff --git a/4.1 changes.txt b/4.1 changes.txt new file mode 100644 index 00000000..d0a4d555 --- /dev/null +++ b/4.1 changes.txt @@ -0,0 +1,3 @@ +IViewMaker.ShowError method dropped the 2nd argument `description` - this is now taken from localized resources +ExceptionReportInfo property `BackgroundColor` is now a hex color code string, not a System.Drawing.Color object +Saving zip file set the file extension to use the template type (ie markdown/txt/html) \ No newline at end of file diff --git a/ExceptionReporter.NET.sln b/ExceptionReporter.NET.sln index 00e21072..0abe2b09 100644 --- a/ExceptionReporter.NET.sln +++ b/ExceptionReporter.NET.sln @@ -3,13 +3,24 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.31402.337 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ExceptionReporter.Net", "src\ExceptionReporting\ExceptionReporter.Net.csproj", "{1AEEC57B-FB44-4F88-AD50-A2A3686DBC97}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ExceptionReporter.WinForms", "src\ExceptionReporter.WinForms\ExceptionReporter.WinForms.csproj", "{1AEEC57B-FB44-4F88-AD50-A2A3686DBC97}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests.ExceptionReporter.Net", "src\Tests\Tests.ExceptionReporter.Net.csproj", "{565583C0-0162-4973-962B-7C3F6EE1A4B0}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Demo.WinForms", "src\Demos\WinForms\Demo.WinForms.csproj", "{2FC9BAC6-3AC4-43C2-A8E2-C9655446531E}" EndProject +Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "ExceptionReporter.Shared", "src\ExceptionReporter.Shared\ExceptionReporter.Shared.shproj", "{AAEEE6CD-9D37-424E-B62A-E7D28D3391F4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExceptionReporter.WPF", "src\ExceptionReporter.WPF\ExceptionReporter.WPF.csproj", "{EF418EF6-D1DB-4CB0-9E77-AFC5D7C64AED}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Demo.WPF", "src\Demos\WPF\Demo.WPF.csproj", "{B2D1A579-07FA-4C8E-90C5-A75790A3FB0E}" +EndProject Global + GlobalSection(SharedMSBuildProjectFiles) = preSolution + src\ExceptionReporter.Shared\ExceptionReporter.Shared.projitems*{1aeec57b-fb44-4f88-ad50-a2a3686dbc97}*SharedItemsImports = 5 + src\ExceptionReporter.Shared\ExceptionReporter.Shared.projitems*{aaeee6cd-9d37-424e-b62a-e7d28d3391f4}*SharedItemsImports = 13 + src\ExceptionReporter.Shared\ExceptionReporter.Shared.projitems*{ef418ef6-d1db-4cb0-9e77-afc5d7c64aed}*SharedItemsImports = 4 + EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU @@ -27,6 +38,14 @@ Global {2FC9BAC6-3AC4-43C2-A8E2-C9655446531E}.Debug|Any CPU.Build.0 = Debug|Any CPU {2FC9BAC6-3AC4-43C2-A8E2-C9655446531E}.Release|Any CPU.ActiveCfg = Release|Any CPU {2FC9BAC6-3AC4-43C2-A8E2-C9655446531E}.Release|Any CPU.Build.0 = Release|Any CPU + {EF418EF6-D1DB-4CB0-9E77-AFC5D7C64AED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EF418EF6-D1DB-4CB0-9E77-AFC5D7C64AED}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EF418EF6-D1DB-4CB0-9E77-AFC5D7C64AED}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EF418EF6-D1DB-4CB0-9E77-AFC5D7C64AED}.Release|Any CPU.Build.0 = Release|Any CPU + {B2D1A579-07FA-4C8E-90C5-A75790A3FB0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2D1A579-07FA-4C8E-90C5-A75790A3FB0E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2D1A579-07FA-4C8E-90C5-A75790A3FB0E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2D1A579-07FA-4C8E-90C5-A75790A3FB0E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/ExceptionReporter.NET.sln.DotSettings b/ExceptionReporter.NET.sln.DotSettings new file mode 100644 index 00000000..564705fe --- /dev/null +++ b/ExceptionReporter.NET.sln.DotSettings @@ -0,0 +1,3 @@ + + True + MAPI \ No newline at end of file diff --git a/src/Demos/WPF/App.config b/src/Demos/WPF/App.config new file mode 100644 index 00000000..193aecc6 --- /dev/null +++ b/src/Demos/WPF/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/Demos/WPF/App.xaml b/src/Demos/WPF/App.xaml new file mode 100644 index 00000000..40fd90dd --- /dev/null +++ b/src/Demos/WPF/App.xaml @@ -0,0 +1,9 @@ + + + + + diff --git a/src/Demos/WPF/App.xaml.cs b/src/Demos/WPF/App.xaml.cs new file mode 100644 index 00000000..ddb5c864 --- /dev/null +++ b/src/Demos/WPF/App.xaml.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Data; +using System.Linq; +using System.Threading.Tasks; +using System.Windows; + +namespace Demo.WPF +{ + /// + /// Interaction logic for App.xaml + /// + public partial class App : Application + { + } +} diff --git a/src/Demos/WPF/Demo.WPF.csproj b/src/Demos/WPF/Demo.WPF.csproj new file mode 100644 index 00000000..94896bd3 --- /dev/null +++ b/src/Demos/WPF/Demo.WPF.csproj @@ -0,0 +1,104 @@ + + + + + Debug + AnyCPU + {B2D1A579-07FA-4C8E-90C5-A75790A3FB0E} + WinExe + Demo.WPF + Demo.WPF + v4.8 + 512 + {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 4 + true + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + 4.0 + + + + + + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + App.xaml + Code + + + DemoWindow.xaml + Code + + + + + Code + + + True + True + Resources.resx + + + True + Settings.settings + True + + + ResXFileCodeGenerator + Resources.Designer.cs + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + + + + + + {ef418ef6-d1db-4cb0-9e77-afc5d7c64aed} + ExceptionReporter.WPF + + + + \ No newline at end of file diff --git a/src/Demos/WPF/DemoWindow.xaml b/src/Demos/WPF/DemoWindow.xaml new file mode 100644 index 00000000..3f3fc9b6 --- /dev/null +++ b/src/Demos/WPF/DemoWindow.xaml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/src/Demos/WPF/DemoWindow.xaml.cs b/src/Demos/WPF/DemoWindow.xaml.cs new file mode 100644 index 00000000..c087454d --- /dev/null +++ b/src/Demos/WPF/DemoWindow.xaml.cs @@ -0,0 +1,50 @@ +using System; +using System.Data; +using System.Windows; +using ExceptionReporting; +using ExceptionReporting.WPF.MvvM.View; + +namespace Demo.WPF +{ + /// + /// Interaction logic for DemoWindow.xaml + /// + public partial class DemoWindow : Window + { + public DemoWindow() + { + InitializeComponent(); + } + + private void ButtonBase_OnClick(object sender, RoutedEventArgs e) + { + try + { + TestMethod(); + } + catch (Exception ex) + { + new Window + { + Title = "Error Report", + Height = 250, + Width = 400, + Content = new WpfExceptionReporter(ex, new ExceptionReportInfo + { + SendMethod = ReportSendMethod.SimpleMAPI, + EmailReportAddress = "support@acme.com", + CompanyName = "Acme", + TitleText = "Acme Error Report", + ShowLessDetailButton = true, + ReportTemplateFormat = TemplateFormat.Text, + }) + }.ShowDialog(); + } + } + + private void TestMethod() + { + throw new DataException("The server responded with warning 477 (Internal error). Error code 777999"); + } + } +} diff --git a/src/Demos/WPF/Properties/AssemblyInfo.cs b/src/Demos/WPF/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..33e7f36f --- /dev/null +++ b/src/Demos/WPF/Properties/AssemblyInfo.cs @@ -0,0 +1,55 @@ +using System.Reflection; +using System.Resources; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Windows; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Demo.WPF2")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Demo.WPF2")] +[assembly: AssemblyCopyright("Copyright © 2021")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +//In order to begin building localizable applications, set +//CultureYouAreCodingWith in your .csproj file +//inside a . For example, if you are using US english +//in your source files, set the to en-US. Then uncomment +//the NeutralResourceLanguage attribute below. Update the "en-US" in +//the line below to match the UICulture setting in the project file. + +//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)] + + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] + + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/Demos/WPF/Properties/Resources.Designer.cs b/src/Demos/WPF/Properties/Resources.Designer.cs new file mode 100644 index 00000000..2489527d --- /dev/null +++ b/src/Demos/WPF/Properties/Resources.Designer.cs @@ -0,0 +1,63 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Demo.WPF.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Demo.WPF.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + } +} diff --git a/src/Demos/WPF/Properties/Resources.resx b/src/Demos/WPF/Properties/Resources.resx new file mode 100644 index 00000000..af7dbebb --- /dev/null +++ b/src/Demos/WPF/Properties/Resources.resx @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/src/Demos/WPF/Properties/Settings.Designer.cs b/src/Demos/WPF/Properties/Settings.Designer.cs new file mode 100644 index 00000000..6aadf775 --- /dev/null +++ b/src/Demos/WPF/Properties/Settings.Designer.cs @@ -0,0 +1,26 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Demo.WPF.Properties { + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.10.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default { + get { + return defaultInstance; + } + } + } +} diff --git a/src/Demos/WPF/Properties/Settings.settings b/src/Demos/WPF/Properties/Settings.settings new file mode 100644 index 00000000..033d7a5e --- /dev/null +++ b/src/Demos/WPF/Properties/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/Demos/WinForms/Demo.WinForms.csproj b/src/Demos/WinForms/Demo.WinForms.csproj index a532d382..aa13cc26 100644 --- a/src/Demos/WinForms/Demo.WinForms.csproj +++ b/src/Demos/WinForms/Demo.WinForms.csproj @@ -65,9 +65,9 @@ - - {c49896a3-c0da-45c4-b30e-4d17e548dbf6} - ExceptionReporter.NET + + {1AEEC57B-FB44-4F88-AD50-A2A3686DBC97} + ExceptionReporter.WinForms diff --git a/src/Demos/WinForms/DemoApp.cs b/src/Demos/WinForms/DemoApp.cs index 18902317..15610c1c 100644 --- a/src/Demos/WinForms/DemoApp.cs +++ b/src/Demos/WinForms/DemoApp.cs @@ -115,8 +115,8 @@ static void ThrowAndShowExceptionReporter(bool detailView = false) er.Config.ShowLessDetailButton = true; er.Config.ReportTemplateFormat = TemplateFormat.Text; // er.Config.ShowEmailButton = false; // just for testing that removing email button works - // er.Config.TemplateFormat = TemplateFormat.Markdown; - + er.Config.ReportTemplateFormat = TemplateFormat.Markdown; + er.Show(exception); } } diff --git a/src/Demos/WinForms/YourCustomReporterView.cs b/src/Demos/WinForms/YourCustomReporterView.cs index d71bcaeb..5cc261a2 100644 --- a/src/Demos/WinForms/YourCustomReporterView.cs +++ b/src/Demos/WinForms/YourCustomReporterView.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Windows.Forms; -using ExceptionReporting.MVP.Views; +using ExceptionReporting.Core; namespace Demo.WinForms { diff --git a/src/Demos/WinForms/YourCustomViewMaker.cs b/src/Demos/WinForms/YourCustomViewMaker.cs index f89d7c8b..4d9dfdd1 100644 --- a/src/Demos/WinForms/YourCustomViewMaker.cs +++ b/src/Demos/WinForms/YourCustomViewMaker.cs @@ -1,5 +1,5 @@ using System.Windows.Forms; -using ExceptionReporting.MVP.Views; +using ExceptionReporting.Core; namespace Demo.WinForms { @@ -13,9 +13,9 @@ public IExceptionReportView Create() return new YourCustomReporterView(); } - public void ShowError(string message, string description) + public void ShowError(string message) { - MessageBox.Show(message, description); + MessageBox.Show(message); } } } \ No newline at end of file diff --git a/src/ExceptionReporter.Shared/Core/ConfigException.cs b/src/ExceptionReporter.Shared/Core/ConfigException.cs new file mode 100644 index 00000000..f96e1a3e --- /dev/null +++ b/src/ExceptionReporter.Shared/Core/ConfigException.cs @@ -0,0 +1,11 @@ +using System; + +namespace ExceptionReporting.Core +{ + internal class ConfigException : Exception + { + public ConfigException(string message) : base(message) + { } + } + +} \ No newline at end of file diff --git a/src/ExceptionReporting/Core/ExceptionReporterExtensions.cs b/src/ExceptionReporter.Shared/Core/ExceptionReporterExtensions.cs similarity index 90% rename from src/ExceptionReporting/Core/ExceptionReporterExtensions.cs rename to src/ExceptionReporter.Shared/Core/ExceptionReporterExtensions.cs index fa587e99..00a479f7 100644 --- a/src/ExceptionReporting/Core/ExceptionReporterExtensions.cs +++ b/src/ExceptionReporter.Shared/Core/ExceptionReporterExtensions.cs @@ -2,7 +2,7 @@ namespace ExceptionReporting.Core { /// /// All extension methods for ExceptionReporter - /// It's important this class is internal/not public - else it will pollute the assembly consumer's extensions + /// It's important this class is internal/not public - else it will pollute the available extensions for users of this library /// internal static class ExceptionReporterExtensions { @@ -19,6 +19,5 @@ public static string Truncate(this string value, int maxLength) if (string.IsNullOrEmpty(value)) return value; return value.Length <= maxLength ? value : value.Substring(0, maxLength); } - } } \ No newline at end of file diff --git a/src/ExceptionReporting/Core/FileService.cs b/src/ExceptionReporter.Shared/Core/FileService.cs similarity index 100% rename from src/ExceptionReporting/Core/FileService.cs rename to src/ExceptionReporter.Shared/Core/FileService.cs diff --git a/src/ExceptionReporting/MVP/Views/IExceptionReportView.cs b/src/ExceptionReporter.Shared/Core/IExceptionReportView.cs similarity index 95% rename from src/ExceptionReporting/MVP/Views/IExceptionReportView.cs rename to src/ExceptionReporter.Shared/Core/IExceptionReportView.cs index 2ec38209..ed127361 100644 --- a/src/ExceptionReporting/MVP/Views/IExceptionReportView.cs +++ b/src/ExceptionReporter.Shared/Core/IExceptionReportView.cs @@ -4,7 +4,7 @@ #pragma warning disable 1591 -namespace ExceptionReporting.MVP.Views +namespace ExceptionReporting.Core { /// /// The interface (contract) for an ExceptionReport dialog/View diff --git a/src/ExceptionReporter.Shared/Core/IScreenShooter.cs b/src/ExceptionReporter.Shared/Core/IScreenShooter.cs new file mode 100644 index 00000000..24c2a695 --- /dev/null +++ b/src/ExceptionReporter.Shared/Core/IScreenShooter.cs @@ -0,0 +1,12 @@ +namespace ExceptionReporting.Core +{ + /// + /// winforms and wpf have different means of taking screenshots, hence this abstraction + /// + public interface IScreenShooter + { + /// + /// + string TakeScreenShot(); + } +} \ No newline at end of file diff --git a/src/ExceptionReporter.Shared/Core/IViewMaker.cs b/src/ExceptionReporter.Shared/Core/IViewMaker.cs new file mode 100644 index 00000000..0184e88b --- /dev/null +++ b/src/ExceptionReporter.Shared/Core/IViewMaker.cs @@ -0,0 +1,19 @@ +using ExceptionReporting.Core; + +/// +/// An interface/contract to replace the default view (currently only applicable to WinForms) +/// +public interface IViewMaker +{ + /// + /// create the main view/dialog + /// + /// + IExceptionReportView Create(); + + /// + /// show an error + /// + /// + void ShowError(string message); +} \ No newline at end of file diff --git a/src/ExceptionReporting/ExceptionReportInfo.cs b/src/ExceptionReporter.Shared/ExceptionReportInfo.cs similarity index 76% rename from src/ExceptionReporting/ExceptionReportInfo.cs rename to src/ExceptionReporter.Shared/ExceptionReportInfo.cs index ab9dc3e8..caf7736a 100644 --- a/src/ExceptionReporting/ExceptionReportInfo.cs +++ b/src/ExceptionReporter.Shared/ExceptionReportInfo.cs @@ -4,10 +4,9 @@ using System; using System.Collections.Generic; -using System.Drawing; using System.Net.Mail; using System.Reflection; -using ExceptionReporting.Properties; +using System.Text; // ReSharper disable UnusedMember.Global // ReSharper disable MemberCanBePrivate.Global @@ -34,7 +33,7 @@ public class ExceptionReportInfo /// public Exception MainException { - get { return _exceptions.Count > 0 ? _exceptions[0] : new Exception("Empty Exception"); } + get => _exceptions.Count > 0 ? _exceptions[0] : new Exception("Empty Exception"); set { _exceptions.Clear(); @@ -45,10 +44,7 @@ public Exception MainException } } - public Exception[] Exceptions - { - get { return _exceptions.ToArray(); } - } + public Exception[] Exceptions => _exceptions.ToArray(); /// /// Add multiple exceptions @@ -70,8 +66,6 @@ public void SetExceptions(IEnumerable exceptions) /// public string CustomMessage { get; set; } - #region SMTP settings - public string SmtpUsername { get; set; } public string SmtpPassword { get; set; } public string SmtpFromAddress { get; set; } = ""; @@ -97,8 +91,6 @@ public void SetExceptions(IEnumerable exceptions) /// public MailPriority SmtpMailPriority { get; set; } = MailPriority.Normal; - #endregion - /// /// The name of the application calling the exception report /// @@ -164,28 +156,18 @@ public void SetExceptions(IEnumerable exceptions) /// public bool ShowExceptionsTab { get; set; } = true; - - private bool _showSysInfoTab = true; + /// /// Show/hide *System Information* (SysInfo) tab in dialog /// ignored in Mono /// - public bool ShowSysInfoTab - { - get { return !ExceptionReporter.IsRunningMono() && _showSysInfoTab; } - set { _showSysInfoTab = value; } - } + public bool ShowSysInfoTab { get; set; } = true; - private bool _showAssembliesTab = true; /// /// Show/hide *Assemblies* tab in dialog /// ignored in Mono /// - public bool ShowAssembliesTab - { - get { return !ExceptionReporter.IsRunningMono() && _showAssembliesTab; } - set { _showAssembliesTab = value; } - } + public bool ShowAssembliesTab { get; set; } = true; /// /// Email address used to send the report via email @@ -219,19 +201,19 @@ public bool ShowAssembliesTab /// public bool ShowEmailButton { - get { return SendMethod != ReportSendMethod.None && _showEmailButton; } - set { _showEmailButton = value; } + get => SendMethod != ReportSendMethod.None && _showEmailButton; + set => _showEmailButton = value; } /// /// The title of the main ExceptionReporter dialog /// - public string TitleText { get; set; } = Resources.ErrorReport; + public string TitleText { get; set; } /// /// Background color of the dialog /// - public Color BackgroundColor { get; set; } = Color.WhiteSmoke; + public string BackgroundColor { get; set; } = "#f5f5f5"; /// /// The font size of the user input text box @@ -239,7 +221,7 @@ public bool ShowEmailButton public float UserExplanationFontSize { get; set; } = 12f; /// - /// Take a screenshot automatically when attaching + /// Take a screenshot automatically when attaching /// which will then be available if sending an email using the ExceptionReporter dialog functionality /// public bool TakeScreenshot { get; set; } = false; @@ -268,16 +250,17 @@ public bool ShowEmailButton /// The attachment filename, extension .zip applied automatically if not provided public string AttachmentFilename { - get { return _attachmentFilename.ToLower().EndsWith(".zip") ? _attachmentFilename : _attachmentFilename + ".zip"; } - set { _attachmentFilename = value; } + get => _attachmentFilename.ToLower().EndsWith(".zip") ? _attachmentFilename : _attachmentFilename + ".zip"; + set => _attachmentFilename = value; } /// /// The text to show in the label that prompts the user to input any relevant message /// - public string UserExplanationLabel { get; set; } = DefaultLabelMessages.DefaultExplanationLabel; + public string UserExplanationLabel { get; set; } - public string ContactMessageTop { get; set; } = DefaultLabelMessages.DefaultContactMessageTop; + [Obsolete("Not actually used anywhere")] + public string ContactMessageTop { get; set; } /// /// Show buttons in the "flat" (non 3D) style @@ -308,7 +291,7 @@ public string AttachmentFilename public TemplateFormat ReportTemplateFormat { get; set; } = TemplateFormat.Text; /// - /// A custom/user Handlebar template (Handlebars is almost identical to Mustache) - see https://handlebarsjs.com + /// A custom/user Handlebar template (Handlebars is almost identical to Mustache) - /// to use instead of the supplied presets /// A populated model will be passed to the template /// See Templates/ReportTemplate.text for example @@ -324,47 +307,6 @@ public bool IsSimpleMAPI() return SendMethod == ReportSendMethod.SimpleMAPI; } } - - /// - /// The communication method used to send a report - /// - public enum ReportSendMethod - { - /// - /// No sending (default) - /// - None, - - /// - /// Tries to use the Windows default Email client eg Outlook via SMTP - /// If a compatible client isn't installed, it will not work, so there is some risk - but in that case, an - /// error message will prompt the user to use the "Copy" feature and manually send the result - /// - /// requires to be set to a valid email - /// - SimpleMAPI, - - /// - /// Sends an Email via an SMTP server - requires other config (host/port etc) properties starting with 'Smtp' - /// - SMTP, - - /// - /// WebService - requires a REST API server accepting content-type 'application/json' of type POST and a - /// JSON packet containing the properties represented in the DataContract class 'ExceptionReportPacket' - /// An example project doing exactly what is required is included in the ExceptionReporter.NET solution - /// - WebService - } - - internal static class DefaultLabelMessages - { - public static readonly string DefaultExplanationLabel = - Resources.Please_enter_a_brief_explanation_of_events_leading_up_to_this_exception; - - public static readonly string DefaultContactMessageTop = - Resources.The_following_details_can_be_used_to_obtain_support_for_this_application; - } } #pragma warning restore 1591 \ No newline at end of file diff --git a/src/ExceptionReporter.Shared/ExceptionReporter.Shared.projitems b/src/ExceptionReporter.Shared/ExceptionReporter.Shared.projitems new file mode 100644 index 00000000..0ed762af --- /dev/null +++ b/src/ExceptionReporter.Shared/ExceptionReporter.Shared.projitems @@ -0,0 +1,71 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + aaeee6cd-9d37-424e-b62a-e7d28d3391f4 + + + ExceptionReporting + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + + + \ No newline at end of file diff --git a/src/ExceptionReporter.Shared/ExceptionReporter.Shared.shproj b/src/ExceptionReporter.Shared/ExceptionReporter.Shared.shproj new file mode 100644 index 00000000..dba5a9c8 --- /dev/null +++ b/src/ExceptionReporter.Shared/ExceptionReporter.Shared.shproj @@ -0,0 +1,13 @@ + + + + aaeee6cd-9d37-424e-b62a-e7d28d3391f4 + 14.0 + + + + + + + + diff --git a/src/ExceptionReporter.Shared/ExceptionReporterBase.cs b/src/ExceptionReporter.Shared/ExceptionReporterBase.cs new file mode 100644 index 00000000..fcfbbc1f --- /dev/null +++ b/src/ExceptionReporter.Shared/ExceptionReporterBase.cs @@ -0,0 +1,63 @@ +/* + * https://github.com/PandaWood/ExceptionReporter.NET + */ + +using System; +using System.Runtime.CompilerServices; +using ExceptionReporting.Core; +using ExceptionReporting.Network; +using ExceptionReporting.Network.Events; +using ExceptionReporting.Report; + +// ReSharper disable ConvertToAutoPropertyWhenPossible +// ReSharper disable MemberCanBePrivate.Global +// ReSharper disable UnusedType.Global + +[assembly: InternalsVisibleTo("Tests.ExceptionReporter.NET")] +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // for moq + +namespace ExceptionReporting +{ + /// + /// A class at a lower-level than ExceptionReporter due to the need to abstract out the option for the + /// implementation of IScreenshooter + /// Call Send() method here directly with any IScreenshooter implementation (including the dummy provided - which will be + /// effectively the same as setting to false) + /// + public class ExceptionReporterBase + { + /// + /// + /// + protected readonly ExceptionReportInfo _info; + + /// + /// Initialise the ExceptionReporter + /// + protected ExceptionReporterBase() + { + _info = new ExceptionReportInfo(); + } + + /// + /// Public access to configuration/settings + /// + public ExceptionReportInfo Config => _info; + + /// + /// Send the report, asynchronously, without showing a dialog (silent send) + /// must be SMTP or WebService, else this is ignored (silently) + /// + /// The screen-shotting code might be specific to WinForms, so this is an option to send anything that implements IScreenshooter + /// Provide implementation of IReportSendEvent to receive error/updates on calling thread + /// The exception/s to include in the report + protected void Send(IScreenShooter screenShooter, IReportSendEvent sendEvent = null, params Exception[] exceptions) + { + _info.SetExceptions(exceptions); + + var sender = new SenderFactory(_info, sendEvent ?? new SilentSendEvent(), screenShooter).Get(); + var report = new ReportGenerator(_info); + sender.Send(report.Generate()); + } + } +} diff --git a/src/ExceptionReporting/Mail/AttachAdapter.cs b/src/ExceptionReporter.Shared/Mail/AttachAdapter.cs similarity index 100% rename from src/ExceptionReporting/Mail/AttachAdapter.cs rename to src/ExceptionReporter.Shared/Mail/AttachAdapter.cs diff --git a/src/ExceptionReporting/Mail/Attacher.cs b/src/ExceptionReporter.Shared/Mail/Attacher.cs similarity index 82% rename from src/ExceptionReporting/Mail/Attacher.cs rename to src/ExceptionReporter.Shared/Mail/Attacher.cs index 6c7ed543..1d15c8bf 100644 --- a/src/ExceptionReporting/Mail/Attacher.cs +++ b/src/ExceptionReporter.Shared/Mail/Attacher.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using ExceptionReporting.Core; +using ExceptionReporting.Zip; namespace ExceptionReporting.Mail { @@ -12,11 +13,13 @@ internal class Attacher public IFileService FileService { private get; set; } = new FileService(); public IZipper Zipper { private get; set; } = new Zipper(); - public IScreenshotTaker ScreenshotTaker { private get; set; } = new ScreenshotTaker(); - public Attacher(ExceptionReportInfo config) + public IScreenShooter ScreenShooter { get; set; } + + public Attacher(ExceptionReportInfo config, IScreenShooter screenShooter) { _config = config; + ScreenShooter = screenShooter; } public void AttachFiles(IAttach attacher) @@ -29,7 +32,7 @@ public void AttachFiles(IAttach attacher) try { - if (_config.TakeScreenshot) files.Add(ScreenshotTaker.TakeScreenShot()); + if (_config.TakeScreenshot) files.Add(ScreenShooter.TakeScreenShot()); } catch { /* ignored */ } var filesThatExist = files.Where(f => FileService.Exists(f)).ToList(); diff --git a/src/ExceptionReporter.Shared/Mail/EmailReporter.cs b/src/ExceptionReporter.Shared/Mail/EmailReporter.cs new file mode 100644 index 00000000..f5d7b302 --- /dev/null +++ b/src/ExceptionReporter.Shared/Mail/EmailReporter.cs @@ -0,0 +1,30 @@ +using ExceptionReporting.Templates; + +namespace ExceptionReporting.Mail +{ + /// + /// an email requires an intro (subject) and a body + /// and both require generation via templates, so here we combine the intro and body together + /// + internal class EmailReporter + { + private readonly ExceptionReportInfo _info; + + public EmailReporter(ExceptionReportInfo info) + { + _info = info; + } + + public string Create() + { + var template = new TemplateRenderer(new EmailIntroModel + { + ScreenshotTaken = _info.TakeScreenshot + }); + var emailIntro = template.RenderPreset(); + var report = new ReportGenerator(_info).Generate(); + + return emailIntro + report; + } + } +} \ No newline at end of file diff --git a/src/ExceptionReporting/Network/Events/ConsoleSendEvent.cs b/src/ExceptionReporter.Shared/Network/Events/ConsoleSendEvent.cs similarity index 100% rename from src/ExceptionReporting/Network/Events/ConsoleSendEvent.cs rename to src/ExceptionReporter.Shared/Network/Events/ConsoleSendEvent.cs diff --git a/src/ExceptionReporting/Network/Events/IReportSendEvent.cs b/src/ExceptionReporter.Shared/Network/Events/IReportSendEvent.cs similarity index 100% rename from src/ExceptionReporting/Network/Events/IReportSendEvent.cs rename to src/ExceptionReporter.Shared/Network/Events/IReportSendEvent.cs diff --git a/src/ExceptionReporting/Network/Events/SilentSendEvent.cs b/src/ExceptionReporter.Shared/Network/Events/SilentSendEvent.cs similarity index 100% rename from src/ExceptionReporting/Network/Events/SilentSendEvent.cs rename to src/ExceptionReporter.Shared/Network/Events/SilentSendEvent.cs diff --git a/src/ExceptionReporting/Network/IReportSender.cs b/src/ExceptionReporter.Shared/Network/IReportSender.cs similarity index 100% rename from src/ExceptionReporting/Network/IReportSender.cs rename to src/ExceptionReporter.Shared/Network/IReportSender.cs diff --git a/src/ExceptionReporting/Network/ReportPacket.cs b/src/ExceptionReporter.Shared/Network/ReportPacket.cs similarity index 100% rename from src/ExceptionReporting/Network/ReportPacket.cs rename to src/ExceptionReporter.Shared/Network/ReportPacket.cs diff --git a/src/ExceptionReporting/Network/SenderFactory.cs b/src/ExceptionReporter.Shared/Network/SenderFactory.cs similarity index 66% rename from src/ExceptionReporting/Network/SenderFactory.cs rename to src/ExceptionReporter.Shared/Network/SenderFactory.cs index cf692f0b..94ac9368 100644 --- a/src/ExceptionReporting/Network/SenderFactory.cs +++ b/src/ExceptionReporter.Shared/Network/SenderFactory.cs @@ -1,4 +1,5 @@ -using ExceptionReporting.Network.Events; +using ExceptionReporting.Core; +using ExceptionReporting.Network.Events; using ExceptionReporting.Network.Senders; namespace ExceptionReporting.Network @@ -7,11 +8,13 @@ internal class SenderFactory { private readonly ExceptionReportInfo _config; private readonly IReportSendEvent _sendEvent; + private readonly IScreenShooter _screenShooter; - public SenderFactory(ExceptionReportInfo config, IReportSendEvent sendEvent) + public SenderFactory(ExceptionReportInfo config, IReportSendEvent sendEvent, IScreenShooter screenShooter) { _config = config; _sendEvent = sendEvent; + _screenShooter = screenShooter; } public IReportSender Get() @@ -21,9 +24,9 @@ public IReportSender Get() case ReportSendMethod.WebService: return new WebServiceSender(_config, _sendEvent); case ReportSendMethod.SMTP: - return new SmtpMailSender(_config, _sendEvent); + return new SmtpMailSender(_config, _sendEvent, _screenShooter); case ReportSendMethod.SimpleMAPI: - return new MapiMailSender(_config, _sendEvent); + return new MapiMailSender(_config, _sendEvent, _screenShooter); case ReportSendMethod.None: return new GhostSender(); default: diff --git a/src/ExceptionReporting/Network/Senders/GhostSender.cs b/src/ExceptionReporter.Shared/Network/Senders/GhostSender.cs similarity index 100% rename from src/ExceptionReporting/Network/Senders/GhostSender.cs rename to src/ExceptionReporter.Shared/Network/Senders/GhostSender.cs diff --git a/src/ExceptionReporting/Network/Senders/MailSender.cs b/src/ExceptionReporter.Shared/Network/Senders/MailSender.cs similarity index 53% rename from src/ExceptionReporting/Network/Senders/MailSender.cs rename to src/ExceptionReporter.Shared/Network/Senders/MailSender.cs index deada225..6cba6e4c 100644 --- a/src/ExceptionReporting/Network/Senders/MailSender.cs +++ b/src/ExceptionReporter.Shared/Network/Senders/MailSender.cs @@ -10,27 +10,22 @@ internal abstract class MailSender protected readonly IReportSendEvent _sendEvent; protected readonly Attacher _attacher; - protected MailSender(ExceptionReportInfo reportInfo, IReportSendEvent sendEvent) + protected MailSender(ExceptionReportInfo reportInfo, IReportSendEvent sendEvent, IScreenShooter screenShooter) { _config = reportInfo; _sendEvent = sendEvent; - _attacher = new Attacher(reportInfo); + _attacher = new Attacher(reportInfo, screenShooter); } public abstract string Description { get; } - public virtual string ConnectingMessage - { - get { return string.Format("Connecting {0}...", Description); } - } + public virtual string ConnectingMessage => $"Connecting {Description}..."; - public string EmailSubject - { - get { return _config.EmailReportSubject .Length > 0 ? _config.EmailReportSubject : - _config.MainException.Message - .Replace('\r', ' ') - .Replace('\n', ' ') - .Truncate(255) ?? "Exception Report"; } - } + public string EmailSubject => + _config.EmailReportSubject .Length > 0 ? _config.EmailReportSubject : + _config.MainException.Message + .Replace('\r', ' ') + .Replace('\n', ' ') + .Truncate(255) ?? "Exception Report"; } } \ No newline at end of file diff --git a/src/ExceptionReporting/Network/Senders/MapiMailSender.cs b/src/ExceptionReporter.Shared/Network/Senders/MapiMailSender.cs similarity index 71% rename from src/ExceptionReporting/Network/Senders/MapiMailSender.cs rename to src/ExceptionReporter.Shared/Network/Senders/MapiMailSender.cs index 0e415f33..0ee84077 100644 --- a/src/ExceptionReporting/Network/Senders/MapiMailSender.cs +++ b/src/ExceptionReporter.Shared/Network/Senders/MapiMailSender.cs @@ -1,4 +1,3 @@ -using System.Configuration; using ExceptionReporting.Core; using ExceptionReporting.Mail; using ExceptionReporting.Network.Events; @@ -8,20 +7,13 @@ namespace ExceptionReporting.Network.Senders { internal class MapiMailSender : MailSender, IReportSender { - public MapiMailSender(ExceptionReportInfo reportInfo, IReportSendEvent sendEvent) : - base(reportInfo, sendEvent) + public MapiMailSender(ExceptionReportInfo reportInfo, IReportSendEvent sendEvent, IScreenShooter screenShooter) : + base(reportInfo, sendEvent, screenShooter) { } - public override string Description - { - get { return "Email Client"; } - } - - public override string ConnectingMessage - { - get { return string.Format("Launching {0}...", Description); } - } - + public override string Description => "Email Client"; + public override string ConnectingMessage => $"Launching {Description}..."; + /// /// Try send via installed Email client /// Uses Simple-MAPI.NET library - https://github.com/PandaWood/Simple-MAPI.NET @@ -30,7 +22,7 @@ public void Send(string report) { if (_config.EmailReportAddress.IsEmpty()) { - _sendEvent.ShowError("EmailReportAddress not set", new ConfigurationErrorsException("EmailReportAddress")); + _sendEvent.ShowError("EmailReportAddress not set", new ConfigException("EmailReportAddress")); return; } diff --git a/src/ExceptionReporting/Network/Senders/SmtpMailSender.cs b/src/ExceptionReporter.Shared/Network/Senders/SmtpMailSender.cs similarity index 85% rename from src/ExceptionReporting/Network/Senders/SmtpMailSender.cs rename to src/ExceptionReporter.Shared/Network/Senders/SmtpMailSender.cs index e29e149f..a525d6d8 100644 --- a/src/ExceptionReporting/Network/Senders/SmtpMailSender.cs +++ b/src/ExceptionReporter.Shared/Network/Senders/SmtpMailSender.cs @@ -2,6 +2,7 @@ using System.Net; using System.Net.Mail; using System.Text; +using ExceptionReporting.Core; using ExceptionReporting.Mail; using ExceptionReporting.Network.Events; @@ -9,14 +10,11 @@ namespace ExceptionReporting.Network.Senders { internal class SmtpMailSender : MailSender, IReportSender { - public SmtpMailSender(ExceptionReportInfo reportInfo, IReportSendEvent sendEvent) : - base(reportInfo, sendEvent) + public SmtpMailSender(ExceptionReportInfo reportInfo, IReportSendEvent sendEvent, IScreenShooter screenShooter) : + base(reportInfo, sendEvent, screenShooter) { } - public override string Description - { - get { return "SMTP"; } - } + public override string Description => "SMTP"; /// /// Send SMTP email, uses native .NET SmtpClient library @@ -66,8 +64,7 @@ private SendCompletedEventHandler SmtpOnSendCompleted(IDisposable message, IDisp else { _sendEvent.Completed(success: false); - _sendEvent.ShowError(string.Format("{0}: ", Description) + - (e.Error.InnerException != null ? e.Error.InnerException.Message : e.Error.Message), e.Error); + _sendEvent.ShowError($"{Description}: " + (e.Error.InnerException != null ? e.Error.InnerException.Message : e.Error.Message), e.Error); } } finally diff --git a/src/ExceptionReporting/Network/Senders/WebServiceSender.cs b/src/ExceptionReporter.Shared/Network/Senders/WebServiceSender.cs similarity index 86% rename from src/ExceptionReporting/Network/Senders/WebServiceSender.cs rename to src/ExceptionReporter.Shared/Network/Senders/WebServiceSender.cs index 271e7205..09c57e52 100644 --- a/src/ExceptionReporting/Network/Senders/WebServiceSender.cs +++ b/src/ExceptionReporter.Shared/Network/Senders/WebServiceSender.cs @@ -19,15 +19,9 @@ internal WebServiceSender(ExceptionReportInfo info, IReportSendEvent sendEvent) _sendEvent = sendEvent; } - public string Description - { - get { return "WebService"; } - } - - public string ConnectingMessage - { - get { return string.Format("Connecting to {0}", Description); } - } + public string Description => "WebService"; + + public string ConnectingMessage => $"Connecting to {Description}"; public void Send(string report) { @@ -69,8 +63,8 @@ private UploadStringCompletedEventHandler OnUploadCompleted(IDisposable webClien else { _sendEvent.Completed(success: false); - _sendEvent.ShowError(string.Format("{0}: ", Description) + - (e.Error.InnerException != null ? e.Error.InnerException.Message : e.Error.Message), e.Error); + _sendEvent.ShowError($"{Description}: " + + (e.Error.InnerException != null ? e.Error.InnerException.Message : e.Error.Message), e.Error); } } finally diff --git a/src/ExceptionReporting/Report/AssemblyDigger.cs b/src/ExceptionReporter.Shared/Report/AssemblyDigger.cs similarity index 100% rename from src/ExceptionReporting/Report/AssemblyDigger.cs rename to src/ExceptionReporter.Shared/Report/AssemblyDigger.cs diff --git a/src/ExceptionReporter.Shared/Report/NoScreenShot.cs b/src/ExceptionReporter.Shared/Report/NoScreenShot.cs new file mode 100644 index 00000000..b550839c --- /dev/null +++ b/src/ExceptionReporter.Shared/Report/NoScreenShot.cs @@ -0,0 +1,19 @@ +using ExceptionReporting.Core; + +namespace ExceptionReporting.Report +{ + /// + /// An implementation of IScreenshooter that does nothing + /// + public class NoScreenShot : IScreenShooter + { + /// + /// Do nothing + /// + /// an empty string + public string TakeScreenShot() + { + return ""; + } + } +} \ No newline at end of file diff --git a/src/ExceptionReporting/Report/ReportBuilder.cs b/src/ExceptionReporter.Shared/Report/ReportBuilder.cs similarity index 100% rename from src/ExceptionReporting/Report/ReportBuilder.cs rename to src/ExceptionReporter.Shared/Report/ReportBuilder.cs diff --git a/src/ExceptionReporting/Report/StackTraceMaker.cs b/src/ExceptionReporter.Shared/Report/StackTraceMaker.cs similarity index 100% rename from src/ExceptionReporting/Report/StackTraceMaker.cs rename to src/ExceptionReporter.Shared/Report/StackTraceMaker.cs diff --git a/src/ExceptionReporting/ReportGenerator.cs b/src/ExceptionReporter.Shared/ReportGenerator.cs similarity index 88% rename from src/ExceptionReporting/ReportGenerator.cs rename to src/ExceptionReporter.Shared/ReportGenerator.cs index 568970b4..f17614d8 100644 --- a/src/ExceptionReporting/ReportGenerator.cs +++ b/src/ExceptionReporter.Shared/ReportGenerator.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Deployment.Application; using System.Reflection; -using System.Windows.Forms; using ExceptionReporting.Core; using ExceptionReporting.Report; using ExceptionReporting.SystemInfo; @@ -35,27 +34,22 @@ public ReportGenerator(ExceptionReportInfo reportInfo) { // this is going to be a dev/learning mistake - fail fast and hard _info = reportInfo ?? throw new ArgumentNullException(nameof(reportInfo)); - - _info.AppName = _info.AppName.IsEmpty() ? Application.ProductName : _info.AppName; - _info.AppVersion = _info.AppVersion.IsEmpty() ? GetAppVersion() : _info.AppVersion; - _info.ExceptionDate = _info.ExceptionDateKind != DateTimeKind.Local ? DateTime.UtcNow : DateTime.Now; - if (_info.AppAssembly == null) + { _info.AppAssembly = Assembly.GetEntryAssembly() ?? Assembly.GetCallingAssembly(); + } + + _info.AppName = _info.AppName.IsEmpty() ? _info.AppAssembly.GetName().Name: _info.AppName; + _info.AppVersion = _info.AppVersion.IsEmpty() ? GetAppVersion() : _info.AppVersion; + _info.ExceptionDate = _info.ExceptionDateKind != DateTimeKind.Local ? DateTime.UtcNow : DateTime.Now; } private string GetAppVersion() { return ApplicationDeployment.IsNetworkDeployed ? - ApplicationDeployment.CurrentDeployment.CurrentVersion.ToString() : Application.ProductVersion; + ApplicationDeployment.CurrentDeployment.CurrentVersion.ToString() : _info.AppAssembly.GetName().Version.ToString(); } -// leave commented out for mono to toggle in/out to be able to compile -// private string GetAppVersion() -// { -// return Application.ProductVersion; -// } - /// /// Generate the exception report /// @@ -81,7 +75,6 @@ public string Generate() /// internal IEnumerable GetOrFetchSysInfoResults() { - if (ExceptionReporter.IsRunningMono()) return new List(); if (_sysInfoResults.Count == 0) _sysInfoResults.AddRange(CreateSysInfoResults()); diff --git a/src/ExceptionReporter.Shared/ReportSendMethod.cs b/src/ExceptionReporter.Shared/ReportSendMethod.cs new file mode 100644 index 00000000..1f558c16 --- /dev/null +++ b/src/ExceptionReporter.Shared/ReportSendMethod.cs @@ -0,0 +1,34 @@ +namespace ExceptionReporting +{ + /// + /// The communication method used to send a report + /// + public enum ReportSendMethod + { + /// + /// No sending (default) + /// + None, + + /// + /// Tries to use the Windows default Email client eg Outlook via SMTP + /// If a compatible client isn't installed, it will not work, so there is some risk - but in that case, an + /// error message will prompt the user to use the "Copy" feature and manually send the result + /// + /// requires to be set to a valid email + /// + SimpleMAPI, + + /// + /// Sends an Email via an SMTP server - requires other config (host/port etc) properties starting with 'Smtp' + /// + SMTP, + + /// + /// WebService - requires a REST API server accepting content-type 'application/json' of type POST and a + /// JSON packet containing the properties represented in the DataContract class 'ExceptionReportPacket' + /// An example project demonstrating requirements is included in the fExceptionReporter.NET solution + /// + WebService + } +} \ No newline at end of file diff --git a/src/ExceptionReporting/SystemInfo/SysInfoQueries.cs b/src/ExceptionReporter.Shared/SystemInfo/SysInfoQueries.cs similarity index 84% rename from src/ExceptionReporting/SystemInfo/SysInfoQueries.cs rename to src/ExceptionReporter.Shared/SystemInfo/SysInfoQueries.cs index 6fa64e41..ae97d6ae 100644 --- a/src/ExceptionReporting/SystemInfo/SysInfoQueries.cs +++ b/src/ExceptionReporter.Shared/SystemInfo/SysInfoQueries.cs @@ -1,5 +1,6 @@ namespace ExceptionReporting.SystemInfo { + // ReSharper disable once ClassNeverInstantiated.Global internal class SysInfoQueries { public static readonly SysInfoQuery OperatingSystem = new SysInfoQuery("Operating System", "Win32_OperatingSystem", false); diff --git a/src/ExceptionReporting/SystemInfo/SysInfoQuery.cs b/src/ExceptionReporter.Shared/SystemInfo/SysInfoQuery.cs similarity index 100% rename from src/ExceptionReporting/SystemInfo/SysInfoQuery.cs rename to src/ExceptionReporter.Shared/SystemInfo/SysInfoQuery.cs diff --git a/src/ExceptionReporting/SystemInfo/SysInfoResult.cs b/src/ExceptionReporter.Shared/SystemInfo/SysInfoResult.cs similarity index 100% rename from src/ExceptionReporting/SystemInfo/SysInfoResult.cs rename to src/ExceptionReporter.Shared/SystemInfo/SysInfoResult.cs diff --git a/src/ExceptionReporting/SystemInfo/SysInfoResultMapper.cs b/src/ExceptionReporter.Shared/SystemInfo/SysInfoResultMapper.cs similarity index 53% rename from src/ExceptionReporting/SystemInfo/SysInfoResultMapper.cs rename to src/ExceptionReporter.Shared/SystemInfo/SysInfoResultMapper.cs index 99a5e66d..12d90874 100644 --- a/src/ExceptionReporting/SystemInfo/SysInfoResultMapper.cs +++ b/src/ExceptionReporter.Shared/SystemInfo/SysInfoResultMapper.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; +using System.Linq; using System.Text; -using System.Windows.Forms; namespace ExceptionReporting.SystemInfo { @@ -16,6 +16,9 @@ internal class SysInfoResultMapper : ISysInfoResultMapper { private readonly IEnumerable _sysInfoResults; + protected SysInfoResultMapper() + { } + public SysInfoResultMapper(IEnumerable sysInfoResults) { _sysInfoResults = sysInfoResults; @@ -36,12 +39,9 @@ public string SysInfoString() { sb.AppendLine("-" + nodeValueParent); - foreach (var childResult in result.ChildResults) + foreach (var nodeValue in result.ChildResults.SelectMany(childResult => childResult.Nodes)) { - foreach (var nodeValue in childResult.Nodes) - { - sb.AppendLine("--" + nodeValue); // the max no. of levels is 2, ie '--' is as deep as we go - } + sb.AppendLine("--" + nodeValue); // the max no. of levels is 2, ie '--' is as deep as we go } } sb.AppendLine(); @@ -49,28 +49,5 @@ public string SysInfoString() return sb.ToString(); } - - /// - /// Add a tree node to an existing parentNode, by passing the SysInfoResult - /// - public static void AddTreeViewNode(TreeNode parentNode, SysInfoResult result) - { - var nodeRoot = new TreeNode(result.Name); - - foreach (var nodeValueParent in result.Nodes) - { - var nodeLeaf = new TreeNode(nodeValueParent); - nodeRoot.Nodes.Add(nodeLeaf); - - foreach (var childResult in result.ChildResults) - { - foreach (var nodeValue in childResult.Nodes) - { - nodeLeaf.Nodes.Add(new TreeNode(nodeValue)); - } - } - } - parentNode.Nodes.Add(nodeRoot); - } } } \ No newline at end of file diff --git a/src/ExceptionReporting/SystemInfo/SysInfoRetriever.cs b/src/ExceptionReporter.Shared/SystemInfo/SysInfoRetriever.cs similarity index 84% rename from src/ExceptionReporting/SystemInfo/SysInfoRetriever.cs rename to src/ExceptionReporter.Shared/SystemInfo/SysInfoRetriever.cs index 13d501c1..f4d68a90 100644 --- a/src/ExceptionReporting/SystemInfo/SysInfoRetriever.cs +++ b/src/ExceptionReporter.Shared/SystemInfo/SysInfoRetriever.cs @@ -20,12 +20,8 @@ internal class SysInfoRetriever /// a SysInfoResult ie containing the results of the query public SysInfoResult Retrieve(SysInfoQuery sysInfoQuery) { - if (ExceptionReporter.IsRunningMono()) - { - return null; - } _sysInfoQuery = sysInfoQuery; - _sysInfoSearcher = new ManagementObjectSearcher(string.Format("SELECT * FROM {0}", _sysInfoQuery.QueryText)); + _sysInfoSearcher = new ManagementObjectSearcher($"SELECT * FROM {_sysInfoQuery.QueryText}"); _sysInfoResult = new SysInfoResult(_sysInfoQuery.Name); foreach (ManagementObject managementObject in _sysInfoSearcher.Get()) @@ -49,7 +45,7 @@ private IEnumerable GetChildren(ManagementBaseObject managementOb childList.Add(childResult); } - var nodeValue = string.Format("{0} = {1}", propertyData.Name, Convert.ToString(propertyData.Value)); + var nodeValue = $"{propertyData.Name} = {Convert.ToString(propertyData.Value)}"; childResult.Nodes.Add(nodeValue); } diff --git a/src/ExceptionReporting/Templates/EmailIntroModel.cs b/src/ExceptionReporter.Shared/Templates/EmailIntroModel.cs similarity index 100% rename from src/ExceptionReporting/Templates/EmailIntroModel.cs rename to src/ExceptionReporter.Shared/Templates/EmailIntroModel.cs diff --git a/src/ExceptionReporting/Templates/EmailIntroTemplate.text b/src/ExceptionReporter.Shared/Templates/EmailIntroTemplate.text similarity index 100% rename from src/ExceptionReporting/Templates/EmailIntroTemplate.text rename to src/ExceptionReporter.Shared/Templates/EmailIntroTemplate.text diff --git a/src/ExceptionReporting/Templates/ReportModel.cs b/src/ExceptionReporter.Shared/Templates/ReportModel.cs similarity index 100% rename from src/ExceptionReporting/Templates/ReportModel.cs rename to src/ExceptionReporter.Shared/Templates/ReportModel.cs diff --git a/src/ExceptionReporting/Templates/ReportTemplate.html b/src/ExceptionReporter.Shared/Templates/ReportTemplate.html similarity index 100% rename from src/ExceptionReporting/Templates/ReportTemplate.html rename to src/ExceptionReporter.Shared/Templates/ReportTemplate.html diff --git a/src/ExceptionReporting/Templates/ReportTemplate.markdown b/src/ExceptionReporter.Shared/Templates/ReportTemplate.markdown similarity index 100% rename from src/ExceptionReporting/Templates/ReportTemplate.markdown rename to src/ExceptionReporter.Shared/Templates/ReportTemplate.markdown diff --git a/src/ExceptionReporting/Templates/ReportTemplate.text b/src/ExceptionReporter.Shared/Templates/ReportTemplate.text similarity index 100% rename from src/ExceptionReporting/Templates/ReportTemplate.text rename to src/ExceptionReporter.Shared/Templates/ReportTemplate.text diff --git a/src/ExceptionReporting/Templates/TemplateFormat.cs b/src/ExceptionReporter.Shared/Templates/TemplateFormat.cs similarity index 100% rename from src/ExceptionReporting/Templates/TemplateFormat.cs rename to src/ExceptionReporter.Shared/Templates/TemplateFormat.cs diff --git a/src/ExceptionReporting/Templates/TemplateRenderer.cs b/src/ExceptionReporter.Shared/Templates/TemplateRenderer.cs similarity index 82% rename from src/ExceptionReporting/Templates/TemplateRenderer.cs rename to src/ExceptionReporter.Shared/Templates/TemplateRenderer.cs index 4e1b7cd0..7e7a994f 100644 --- a/src/ExceptionReporting/Templates/TemplateRenderer.cs +++ b/src/ExceptionReporter.Shared/Templates/TemplateRenderer.cs @@ -1,4 +1,5 @@ -using System.IO; +using System; +using System.IO; using System.Reflection; using System.Text; using HandlebarsDotNet; @@ -45,12 +46,12 @@ public string RenderCustom(string template) private string GetTemplate(TemplateFormat format) { - var resource = string.Format("{0}.{1}.{2}", this.GetType().Namespace, _name, format.ToString().ToLower()); + var resource = $"{this.GetType().Namespace}.{_name}.{format.ToString().ToLower()}"; var assembly = Assembly.GetExecutingAssembly(); using (var stream = assembly.GetManifestResourceStream(resource)) { - using (var reader = new StreamReader(stream, Encoding.UTF8)) + using (var reader = new StreamReader(stream ?? throw new InvalidOperationException($"resource not found: {resource}"), Encoding.UTF8)) { var template = reader.ReadToEnd(); return template; diff --git a/src/ExceptionReporting/Core/IZipReportService.cs b/src/ExceptionReporter.Shared/Zip/IZipAttachmentService.cs similarity index 88% rename from src/ExceptionReporting/Core/IZipReportService.cs rename to src/ExceptionReporter.Shared/Zip/IZipAttachmentService.cs index 4e400aeb..ad71db4b 100644 --- a/src/ExceptionReporting/Core/IZipReportService.cs +++ b/src/ExceptionReporter.Shared/Zip/IZipAttachmentService.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; -namespace ExceptionReporting.Core +namespace ExceptionReporting.Zip { - internal interface IZipReportService + internal interface IZipAttachmentService { /// /// Create Zip file diff --git a/src/ExceptionReporter.Shared/Zip/ReportFileZipper.cs b/src/ExceptionReporter.Shared/Zip/ReportFileZipper.cs new file mode 100644 index 00000000..93fe9880 --- /dev/null +++ b/src/ExceptionReporter.Shared/Zip/ReportFileZipper.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using System.IO; +using ExceptionReporting.Core; +using ExceptionReporting.Report; + +namespace ExceptionReporting.Zip +{ + internal class ReportFileZipper + { + private readonly IFileService _fileService; + private readonly ReportGenerator _reportGenerator; + private readonly ExceptionReportInfo _info; + + public ReportFileZipper(IFileService fileService, ReportGenerator reportGenerator, ExceptionReportInfo info) + { + _fileService = fileService; + _reportGenerator = reportGenerator; + _info = info; + } + + public FileSaveResult Save(string zipFilePath) + { + var fileExtension = _info.ReportTemplateFormat.ToString().ToLower().Replace("text", "txt"); + var reportPath = Path.Combine(Path.GetTempPath(), $"ExceptionReporter{Path.DirectorySeparatorChar}report.{fileExtension}"); + if (!Directory.Exists(reportPath)) + { + Directory.CreateDirectory(Path.GetDirectoryName(reportPath)); + } + + var report = _reportGenerator.Generate(); + var saveResult = _fileService.Write(reportPath, report); + if (saveResult.Saved) + { + var zipReport = new ZipAttachmentService(new Zipper(), new NoScreenShot(), _fileService); + var savedPath = zipReport.CreateZipReport(_info, zipFilePath, new List {reportPath}); + if (!File.Exists(savedPath)) + { // we might be guilty of using exceptions to control flow here, but it's complying with existing design, so maybe we can let it slide + return new FileSaveResult {Exception = new IOException(savedPath)}; + } + } + return saveResult; + } + } +} \ No newline at end of file diff --git a/src/ExceptionReporting/Core/ZipReportService.cs b/src/ExceptionReporter.Shared/Zip/ZipAttachmentService.cs similarity index 67% rename from src/ExceptionReporting/Core/ZipReportService.cs rename to src/ExceptionReporter.Shared/Zip/ZipAttachmentService.cs index 216bcb9c..720a941f 100644 --- a/src/ExceptionReporting/Core/ZipReportService.cs +++ b/src/ExceptionReporter.Shared/Zip/ZipAttachmentService.cs @@ -1,19 +1,19 @@ using System.Collections.Generic; using System.Linq; -using ExceptionReporting.Mail; +using ExceptionReporting.Core; -namespace ExceptionReporting.Core +namespace ExceptionReporting.Zip { - internal class ZipReportService : IZipReportService + internal class ZipAttachmentService : IZipAttachmentService { private IZipper Zipper { get; } - private IScreenshotTaker ScreenshotTaker { get; } + private IScreenShooter ScreenShooter { get; } private IFileService FileService { get; } - public ZipReportService(IZipper zipper, IScreenshotTaker screenshotTaker, IFileService fileService) + public ZipAttachmentService(IZipper zipper, IScreenShooter screenShooter, IFileService fileService) { Zipper = zipper; - ScreenshotTaker = screenshotTaker; + ScreenShooter = screenShooter; FileService = fileService; } @@ -32,7 +32,7 @@ public string CreateZipReport(ExceptionReportInfo reportInfo, string zipFilePath if (additionalFiles?.Count > 0) files.AddRange(additionalFiles); try { - if (reportInfo.TakeScreenshot) files.Add(ScreenshotTaker.TakeScreenShot()); + if (reportInfo.TakeScreenshot) files.Add(ScreenShooter.TakeScreenShot()); } catch { @@ -40,10 +40,9 @@ public string CreateZipReport(ExceptionReportInfo reportInfo, string zipFilePath } var filesThatExist = files.Where(f => FileService.Exists(f)).ToList(); - var filesToZip = filesThatExist; - if (filesToZip.Any()) - Zipper.Zip(zipFilePath, filesToZip); + if (filesThatExist.Any()) + Zipper.Zip(zipFilePath, filesThatExist); else return string.Empty; diff --git a/src/ExceptionReporting/Mail/Zipper.cs b/src/ExceptionReporter.Shared/Zip/Zipper.cs similarity index 82% rename from src/ExceptionReporting/Mail/Zipper.cs rename to src/ExceptionReporter.Shared/Zip/Zipper.cs index 9b866289..fc3ab3b8 100644 --- a/src/ExceptionReporting/Mail/Zipper.cs +++ b/src/ExceptionReporter.Shared/Zip/Zipper.cs @@ -1,7 +1,7 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Ionic.Zip; -namespace ExceptionReporting.Mail +namespace ExceptionReporting.Zip { internal interface IZipper { diff --git a/src/ExceptionReporter.WPF/Converters/IconToImageSourceConverter.cs b/src/ExceptionReporter.WPF/Converters/IconToImageSourceConverter.cs new file mode 100644 index 00000000..44b721d4 --- /dev/null +++ b/src/ExceptionReporter.WPF/Converters/IconToImageSourceConverter.cs @@ -0,0 +1,37 @@ +using System; +using System.Diagnostics; +using System.Drawing; +using System.Globalization; +using System.Windows; +using System.Windows.Data; +using System.Windows.Interop; +using System.Windows.Media; +using System.Windows.Media.Imaging; + +// ReSharper disable once CheckNamespace +namespace ExceptionReporting.WPF.Converters +{ + public class IconToImageSourceConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + var icon = value as Icon; + if (icon == null) + { + Trace.TraceWarning("Attempted to convert {0} instead of Icon object in IconToImageSourceConverter", value); + return null; + } + + ImageSource imageSource = Imaging.CreateBitmapSourceFromHIcon( + icon.Handle, + Int32Rect.Empty, + BitmapSizeOptions.FromEmptyOptions()); + return imageSource; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/src/ExceptionReporter.WPF/ExceptionReporter.WPF.csproj b/src/ExceptionReporter.WPF/ExceptionReporter.WPF.csproj new file mode 100644 index 00000000..4131431d --- /dev/null +++ b/src/ExceptionReporter.WPF/ExceptionReporter.WPF.csproj @@ -0,0 +1,112 @@ + + + + + Debug + AnyCPU + {EF418EF6-D1DB-4CB0-9E77-AFC5D7C64AED} + library + ExceptionReporting + ExceptionReporter.WPF + v4.8 + 512 + {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 4 + true + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + true + + + + + + + + + + + + + + + + 4.0 + + + + + + + + MSBuild:Compile + Designer + + + + WpfExceptionReporter.xaml + Code + + + + + + + + + Code + + + True + Settings.settings + True + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + + + + + + + + + + + + + + + 1.15.0 + + + 1.9.0 + + + 1.1.0 + + + + + \ No newline at end of file diff --git a/src/ExceptionReporter.WPF/Images/clipboard.png b/src/ExceptionReporter.WPF/Images/clipboard.png new file mode 100644 index 00000000..eb14d21e Binary files /dev/null and b/src/ExceptionReporter.WPF/Images/clipboard.png differ diff --git a/src/ExceptionReporter.WPF/Images/magnifying-glass.png b/src/ExceptionReporter.WPF/Images/magnifying-glass.png new file mode 100644 index 00000000..85714ce7 Binary files /dev/null and b/src/ExceptionReporter.WPF/Images/magnifying-glass.png differ diff --git a/src/ExceptionReporter.WPF/Images/message.png b/src/ExceptionReporter.WPF/Images/message.png new file mode 100644 index 00000000..b46d6256 Binary files /dev/null and b/src/ExceptionReporter.WPF/Images/message.png differ diff --git a/src/ExceptionReporter.WPF/MvvM/View/WpfExceptionReporter.xaml b/src/ExceptionReporter.WPF/MvvM/View/WpfExceptionReporter.xaml new file mode 100644 index 00000000..872ad163 --- /dev/null +++ b/src/ExceptionReporter.WPF/MvvM/View/WpfExceptionReporter.xaml @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Show details + + + + + + + + + + diff --git a/src/ExceptionReporter.WPF/MvvM/View/WpfExceptionReporter.xaml.cs b/src/ExceptionReporter.WPF/MvvM/View/WpfExceptionReporter.xaml.cs new file mode 100644 index 00000000..93c2e483 --- /dev/null +++ b/src/ExceptionReporter.WPF/MvvM/View/WpfExceptionReporter.xaml.cs @@ -0,0 +1,21 @@ +using System; +using System.Windows.Controls; +using ExceptionReporting.WPF.MvvM.ViewModel; + +// ReSharper disable CheckNamespace +namespace ExceptionReporting.WPF.MvvM.View +{ + /// + /// Interaction logic for WpfExceptionReporter + /// + public partial class WpfExceptionReporter : UserControl + { + public WpfExceptionReporter(Exception exception, ExceptionReportInfo info) + { + InitializeComponent(); + + info.MainException = exception; + this.DataContext = new ExceptionReporterViewModel(info); + } + } +} \ No newline at end of file diff --git a/src/ExceptionReporter.WPF/MvvM/ViewModel/ExceptionReporterViewModel.cs b/src/ExceptionReporter.WPF/MvvM/ViewModel/ExceptionReporterViewModel.cs new file mode 100644 index 00000000..a7714b58 --- /dev/null +++ b/src/ExceptionReporter.WPF/MvvM/ViewModel/ExceptionReporterViewModel.cs @@ -0,0 +1,86 @@ +using System.ComponentModel; +using System.Windows; +using System.Windows.Input; +using ExceptionReporting.Mail; +using ExceptionReporting.Network; +using ExceptionReporting.Network.Events; +using ExceptionReporting.Plumbing; +using ExceptionReporting.Report; + +// ReSharper disable once CheckNamespace +namespace ExceptionReporting.WPF.MvvM.ViewModel +{ + public class ExceptionReporterViewModel : ObservableObject + { + private RelayCommand _copyCommand; + private RelayCommand _emailCommand; + private RelayCommand _showDetailsCommand; + + public ExceptionReportInfo Info { get; } + private readonly ReportGenerator _reportGenerator; + private bool _showingDetails; + private string _reportText; + + public ExceptionReporterViewModel(ExceptionReportInfo info) + { + Info = info; + _reportGenerator = new ReportGenerator(Info); + _reportText = _reportGenerator.Generate(); + } + + public ICommand CopyCommand + { + get { return _copyCommand ?? (_copyCommand = new RelayCommand(_ => Copy(), _ => true)); } + } + + public ICommand EmailCommand + { + get { return _emailCommand ?? (_emailCommand = new RelayCommand(_ => SendEmail(), _ => true)); } + } + + public ICommand ShowDetailsCommand + { + get { return _showDetailsCommand ?? (_showDetailsCommand = new RelayCommand(_ => ShowDetails(), _ => true)); } + } + + public string ReportText + { + get => _reportText; + set + { + _reportText = value; + NotifyPropertyChanged(); + } + } + + public bool ShowingSummary => !ShowingDetails; + + public bool ShowingDetails + { + get => _showingDetails; + set + { + _showingDetails = value; + NotifyPropertyChanged(); + NotifyPropertyChanged(nameof(ShowingSummary)); + } + } + + private void Copy() + { + Clipboard.SetText(_reportGenerator.Generate()); + } + + private void SendEmail() + { + var report = Info.IsSimpleMAPI() ? new EmailReporter(Info).Create() : _reportGenerator.Generate(); + var sendFactory = new SenderFactory(Info, new SilentSendEvent(), new NoScreenShot()).Get(); + sendFactory.Send(report); + } + + private void ShowDetails() + { + ShowingDetails = !ShowingDetails; + } + } +} \ No newline at end of file diff --git a/src/ExceptionReporter.WPF/Plumbing/ObservableObject.cs b/src/ExceptionReporter.WPF/Plumbing/ObservableObject.cs new file mode 100644 index 00000000..98844277 --- /dev/null +++ b/src/ExceptionReporter.WPF/Plumbing/ObservableObject.cs @@ -0,0 +1,17 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; +using ExceptionReporting.Annotations; + +namespace ExceptionReporting.Plumbing +{ + public class ObservableObject : INotifyPropertyChanged + { + public event PropertyChangedEventHandler PropertyChanged; + + [NotifyPropertyChangedInvocator] + protected void NotifyPropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } +} \ No newline at end of file diff --git a/src/ExceptionReporter.WPF/Plumbing/RelayCommand.cs b/src/ExceptionReporter.WPF/Plumbing/RelayCommand.cs new file mode 100644 index 00000000..5b0f6571 --- /dev/null +++ b/src/ExceptionReporter.WPF/Plumbing/RelayCommand.cs @@ -0,0 +1,30 @@ +using System; +using System.Diagnostics; +using System.Windows.Input; + +namespace ExceptionReporting.Plumbing +{ + public class RelayCommand : ICommand + { + readonly Action _execute; + readonly Predicate _canExecute; + + public RelayCommand(Action execute, Predicate canExecute = null) + { + _execute = execute ?? throw new ArgumentNullException(nameof(execute)); _canExecute = canExecute; + } + + [DebuggerStepThrough] + public bool CanExecute(object parameter) + { + return _canExecute == null || _canExecute(parameter); + } + + public event EventHandler CanExecuteChanged + { + add => CommandManager.RequerySuggested += value; + remove => CommandManager.RequerySuggested -= value; + } + public void Execute(object parameter) { _execute(parameter); } + } +} \ No newline at end of file diff --git a/src/ExceptionReporter.WPF/Properties/Annotations.cs b/src/ExceptionReporter.WPF/Properties/Annotations.cs new file mode 100644 index 00000000..11bae574 --- /dev/null +++ b/src/ExceptionReporter.WPF/Properties/Annotations.cs @@ -0,0 +1,1236 @@ +/* MIT License + +Copyright (c) 2016 JetBrains http://www.jetbrains.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. */ + +using System; +// ReSharper disable InheritdocConsiderUsage + +#pragma warning disable 1591 +// ReSharper disable UnusedMember.Global +// ReSharper disable MemberCanBePrivate.Global +// ReSharper disable UnusedAutoPropertyAccessor.Global +// ReSharper disable IntroduceOptionalParameters.Global +// ReSharper disable MemberCanBeProtected.Global +// ReSharper disable InconsistentNaming + +namespace ExceptionReporting.Annotations +{ + /// + /// Indicates that the value of the marked element could be null sometimes, + /// so checking for null is required before its usage. + /// + /// + /// [CanBeNull] object Test() => null; + /// + /// void UseTest() { + /// var p = Test(); + /// var s = p.ToString(); // Warning: Possible 'System.NullReferenceException' + /// } + /// + [AttributeUsage( + AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property | + AttributeTargets.Delegate | AttributeTargets.Field | AttributeTargets.Event | + AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.GenericParameter)] + public sealed class CanBeNullAttribute : Attribute { } + + /// + /// Indicates that the value of the marked element can never be null. + /// + /// + /// [NotNull] object Foo() { + /// return null; // Warning: Possible 'null' assignment + /// } + /// + [AttributeUsage( + AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property | + AttributeTargets.Delegate | AttributeTargets.Field | AttributeTargets.Event | + AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.GenericParameter)] + public sealed class NotNullAttribute : Attribute { } + + /// + /// Can be applied to symbols of types derived from IEnumerable as well as to symbols of Task + /// and Lazy classes to indicate that the value of a collection item, of the Task.Result property + /// or of the Lazy.Value property can never be null. + /// + /// + /// public void Foo([ItemNotNull]List<string> books) + /// { + /// foreach (var book in books) { + /// if (book != null) // Warning: Expression is always true + /// Console.WriteLine(book.ToUpper()); + /// } + /// } + /// + [AttributeUsage( + AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property | + AttributeTargets.Delegate | AttributeTargets.Field)] + public sealed class ItemNotNullAttribute : Attribute { } + + /// + /// Can be applied to symbols of types derived from IEnumerable as well as to symbols of Task + /// and Lazy classes to indicate that the value of a collection item, of the Task.Result property + /// or of the Lazy.Value property can be null. + /// + /// + /// public void Foo([ItemCanBeNull]List<string> books) + /// { + /// foreach (var book in books) + /// { + /// // Warning: Possible 'System.NullReferenceException' + /// Console.WriteLine(book.ToUpper()); + /// } + /// } + /// + [AttributeUsage( + AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property | + AttributeTargets.Delegate | AttributeTargets.Field)] + public sealed class ItemCanBeNullAttribute : Attribute { } + + /// + /// Indicates that the marked method builds string by the format pattern and (optional) arguments. + /// The parameter, which contains the format string, should be given in constructor. The format string + /// should be in -like form. + /// + /// + /// [StringFormatMethod("message")] + /// void ShowError(string message, params object[] args) { /* do something */ } + /// + /// void Foo() { + /// ShowError("Failed: {0}"); // Warning: Non-existing argument in format string + /// } + /// + [AttributeUsage( + AttributeTargets.Constructor | AttributeTargets.Method | + AttributeTargets.Property | AttributeTargets.Delegate)] + public sealed class StringFormatMethodAttribute : Attribute + { + /// + /// Specifies which parameter of an annotated method should be treated as the format string + /// + public StringFormatMethodAttribute([NotNull] string formatParameterName) + { + FormatParameterName = formatParameterName; + } + + [NotNull] public string FormatParameterName { get; } + } + + /// + /// Use this annotation to specify a type that contains static or const fields + /// with values for the annotated property/field/parameter. + /// The specified type will be used to improve completion suggestions. + /// + /// + /// namespace TestNamespace + /// { + /// public class Constants + /// { + /// public static int INT_CONST = 1; + /// public const string STRING_CONST = "1"; + /// } + /// + /// public class Class1 + /// { + /// [ValueProvider("TestNamespace.Constants")] public int myField; + /// public void Foo([ValueProvider("TestNamespace.Constants")] string str) { } + /// + /// public void Test() + /// { + /// Foo(/*try completion here*/);// + /// myField = /*try completion here*/ + /// } + /// } + /// } + /// + [AttributeUsage( + AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.Field, + AllowMultiple = true)] + public sealed class ValueProviderAttribute : Attribute + { + public ValueProviderAttribute([NotNull] string name) + { + Name = name; + } + + [NotNull] public string Name { get; } + } + + /// + /// Indicates that the integral value falls into the specified interval. + /// It's allowed to specify multiple non-intersecting intervals. + /// Values of interval boundaries are inclusive. + /// + /// + /// void Foo([ValueRange(0, 100)] int value) { + /// if (value == -1) { // Warning: Expression is always 'false' + /// ... + /// } + /// } + /// + [AttributeUsage( + AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property | + AttributeTargets.Method | AttributeTargets.Delegate, + AllowMultiple = true)] + public sealed class ValueRangeAttribute : Attribute + { + public object From { get; } + public object To { get; } + + public ValueRangeAttribute(long from, long to) + { + From = from; + To = to; + } + + public ValueRangeAttribute(ulong from, ulong to) + { + From = from; + To = to; + } + + public ValueRangeAttribute(long value) + { + From = To = value; + } + + public ValueRangeAttribute(ulong value) + { + From = To = value; + } + } + + /// + /// Indicates that the integral value never falls below zero. + /// + /// + /// void Foo([NonNegativeValue] int value) { + /// if (value == -1) { // Warning: Expression is always 'false' + /// ... + /// } + /// } + /// + [AttributeUsage( + AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property | + AttributeTargets.Method | AttributeTargets.Delegate)] + public sealed class NonNegativeValueAttribute : Attribute { } + + /// + /// Indicates that the function argument should be a string literal and match one + /// of the parameters of the caller function. For example, ReSharper annotates + /// the parameter of . + /// + /// + /// void Foo(string param) { + /// if (param == null) + /// throw new ArgumentNullException("par"); // Warning: Cannot resolve symbol + /// } + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class InvokerParameterNameAttribute : Attribute { } + + /// + /// Indicates that the method is contained in a type that implements + /// System.ComponentModel.INotifyPropertyChanged interface and this method + /// is used to notify that some property value changed. + /// + /// + /// The method should be non-static and conform to one of the supported signatures: + /// + /// NotifyChanged(string) + /// NotifyChanged(params string[]) + /// NotifyChanged{T}(Expression{Func{T}}) + /// NotifyChanged{T,U}(Expression{Func{T,U}}) + /// SetProperty{T}(ref T, T, string) + /// + /// + /// + /// public class Foo : INotifyPropertyChanged { + /// public event PropertyChangedEventHandler PropertyChanged; + /// + /// [NotifyPropertyChangedInvocator] + /// protected virtual void NotifyChanged(string propertyName) { ... } + /// + /// string _name; + /// + /// public string Name { + /// get { return _name; } + /// set { _name = value; NotifyChanged("LastName"); /* Warning */ } + /// } + /// } + /// + /// Examples of generated notifications: + /// + /// NotifyChanged("Property") + /// NotifyChanged(() => Property) + /// NotifyChanged((VM x) => x.Property) + /// SetProperty(ref myField, value, "Property") + /// + /// + [AttributeUsage(AttributeTargets.Method)] + public sealed class NotifyPropertyChangedInvocatorAttribute : Attribute + { + public NotifyPropertyChangedInvocatorAttribute() { } + public NotifyPropertyChangedInvocatorAttribute([NotNull] string parameterName) + { + ParameterName = parameterName; + } + + [CanBeNull] public string ParameterName { get; } + } + + /// + /// Describes dependency between method input and output. + /// + /// + ///

Function Definition Table syntax:

+ /// + /// FDT ::= FDTRow [;FDTRow]* + /// FDTRow ::= Input => Output | Output <= Input + /// Input ::= ParameterName: Value [, Input]* + /// Output ::= [ParameterName: Value]* {halt|stop|void|nothing|Value} + /// Value ::= true | false | null | notnull | canbenull + /// + /// If the method has a single input parameter, its name could be omitted.
+ /// Using halt (or void/nothing, which is the same) for the method output + /// means that the method doesn't return normally (throws or terminates the process).
+ /// Value canbenull is only applicable for output parameters.
+ /// You can use multiple [ContractAnnotation] for each FDT row, or use single attribute + /// with rows separated by semicolon. There is no notion of order rows, all rows are checked + /// for applicability and applied per each program state tracked by the analysis engine.
+ ///
+ /// + /// + /// [ContractAnnotation("=> halt")] + /// public void TerminationMethod() + /// + /// + /// [ContractAnnotation("null <= param:null")] // reverse condition syntax + /// public string GetName(string surname) + /// + /// + /// [ContractAnnotation("s:null => true")] + /// public bool IsNullOrEmpty(string s) // string.IsNullOrEmpty() + /// + /// + /// // A method that returns null if the parameter is null, + /// // and not null if the parameter is not null + /// [ContractAnnotation("null => null; notnull => notnull")] + /// public object Transform(object data) + /// + /// + /// [ContractAnnotation("=> true, result: notnull; => false, result: null")] + /// public bool TryParse(string s, out Person result) + /// + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + public sealed class ContractAnnotationAttribute : Attribute + { + public ContractAnnotationAttribute([NotNull] string contract) + : this(contract, false) { } + + public ContractAnnotationAttribute([NotNull] string contract, bool forceFullStates) + { + Contract = contract; + ForceFullStates = forceFullStates; + } + + [NotNull] public string Contract { get; } + + public bool ForceFullStates { get; } + } + + /// + /// Indicates whether the marked element should be localized. + /// + /// + /// [LocalizationRequiredAttribute(true)] + /// class Foo { + /// string str = "my string"; // Warning: Localizable string + /// } + /// + [AttributeUsage(AttributeTargets.All)] + public sealed class LocalizationRequiredAttribute : Attribute + { + public LocalizationRequiredAttribute() : this(true) { } + + public LocalizationRequiredAttribute(bool required) + { + Required = required; + } + + public bool Required { get; } + } + + /// + /// Indicates that the value of the marked type (or its derivatives) + /// cannot be compared using '==' or '!=' operators and Equals() + /// should be used instead. However, using '==' or '!=' for comparison + /// with null is always permitted. + /// + /// + /// [CannotApplyEqualityOperator] + /// class NoEquality { } + /// + /// class UsesNoEquality { + /// void Test() { + /// var ca1 = new NoEquality(); + /// var ca2 = new NoEquality(); + /// if (ca1 != null) { // OK + /// bool condition = ca1 == ca2; // Warning + /// } + /// } + /// } + /// + [AttributeUsage(AttributeTargets.Interface | AttributeTargets.Class | AttributeTargets.Struct)] + public sealed class CannotApplyEqualityOperatorAttribute : Attribute { } + + /// + /// When applied to a target attribute, specifies a requirement for any type marked + /// with the target attribute to implement or inherit specific type or types. + /// + /// + /// [BaseTypeRequired(typeof(IComponent)] // Specify requirement + /// class ComponentAttribute : Attribute { } + /// + /// [Component] // ComponentAttribute requires implementing IComponent interface + /// class MyComponent : IComponent { } + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + [BaseTypeRequired(typeof(Attribute))] + public sealed class BaseTypeRequiredAttribute : Attribute + { + public BaseTypeRequiredAttribute([NotNull] Type baseType) + { + BaseType = baseType; + } + + [NotNull] public Type BaseType { get; } + } + + /// + /// Indicates that the marked symbol is used implicitly (e.g. via reflection, in external library), + /// so this symbol will not be reported as unused (as well as by other usage inspections). + /// + [AttributeUsage(AttributeTargets.All)] + public sealed class UsedImplicitlyAttribute : Attribute + { + public UsedImplicitlyAttribute() + : this(ImplicitUseKindFlags.Default, ImplicitUseTargetFlags.Default) { } + + public UsedImplicitlyAttribute(ImplicitUseKindFlags useKindFlags) + : this(useKindFlags, ImplicitUseTargetFlags.Default) { } + + public UsedImplicitlyAttribute(ImplicitUseTargetFlags targetFlags) + : this(ImplicitUseKindFlags.Default, targetFlags) { } + + public UsedImplicitlyAttribute(ImplicitUseKindFlags useKindFlags, ImplicitUseTargetFlags targetFlags) + { + UseKindFlags = useKindFlags; + TargetFlags = targetFlags; + } + + public ImplicitUseKindFlags UseKindFlags { get; } + + public ImplicitUseTargetFlags TargetFlags { get; } + } + + /// + /// Can be applied to attributes, type parameters, and parameters of a type assignable from . + /// When applied to an attribute, the decorated attribute behaves the same as . + /// When applied to a type parameter or to a parameter of type , indicates that the corresponding type + /// is used implicitly. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.GenericParameter | AttributeTargets.Parameter)] + public sealed class MeansImplicitUseAttribute : Attribute + { + public MeansImplicitUseAttribute() + : this(ImplicitUseKindFlags.Default, ImplicitUseTargetFlags.Default) { } + + public MeansImplicitUseAttribute(ImplicitUseKindFlags useKindFlags) + : this(useKindFlags, ImplicitUseTargetFlags.Default) { } + + public MeansImplicitUseAttribute(ImplicitUseTargetFlags targetFlags) + : this(ImplicitUseKindFlags.Default, targetFlags) { } + + public MeansImplicitUseAttribute(ImplicitUseKindFlags useKindFlags, ImplicitUseTargetFlags targetFlags) + { + UseKindFlags = useKindFlags; + TargetFlags = targetFlags; + } + + [UsedImplicitly] public ImplicitUseKindFlags UseKindFlags { get; } + + [UsedImplicitly] public ImplicitUseTargetFlags TargetFlags { get; } + } + + /// + /// Specify the details of implicitly used symbol when it is marked + /// with or . + /// + [Flags] + public enum ImplicitUseKindFlags + { + Default = Access | Assign | InstantiatedWithFixedConstructorSignature, + /// Only entity marked with attribute considered used. + Access = 1, + /// Indicates implicit assignment to a member. + Assign = 2, + /// + /// Indicates implicit instantiation of a type with fixed constructor signature. + /// That means any unused constructor parameters won't be reported as such. + /// + InstantiatedWithFixedConstructorSignature = 4, + /// Indicates implicit instantiation of a type. + InstantiatedNoFixedConstructorSignature = 8, + } + + /// + /// Specify what is considered to be used implicitly when marked + /// with or . + /// + [Flags] + public enum ImplicitUseTargetFlags + { + Default = Itself, + Itself = 1, + /// Members of entity marked with attribute are considered used. + Members = 2, + /// Inherited entities are considered used. + WithInheritors = 4, + /// Entity marked with attribute and all its members considered used. + WithMembers = Itself | Members + } + + /// + /// This attribute is intended to mark publicly available API + /// which should not be removed and so is treated as used. + /// + [MeansImplicitUse(ImplicitUseTargetFlags.WithMembers)] + [AttributeUsage(AttributeTargets.All, Inherited = false)] + public sealed class PublicAPIAttribute : Attribute + { + public PublicAPIAttribute() { } + + public PublicAPIAttribute([NotNull] string comment) + { + Comment = comment; + } + + [CanBeNull] public string Comment { get; } + } + + /// + /// Tells code analysis engine if the parameter is completely handled when the invoked method is on stack. + /// If the parameter is a delegate, indicates that delegate is executed while the method is executed. + /// If the parameter is an enumerable, indicates that it is enumerated while the method is executed. + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class InstantHandleAttribute : Attribute { } + + /// + /// Indicates that a method does not make any observable state changes. + /// The same as System.Diagnostics.Contracts.PureAttribute. + /// + /// + /// [Pure] int Multiply(int x, int y) => x * y; + /// + /// void M() { + /// Multiply(123, 42); // Warning: Return value of pure method is not used + /// } + /// + [AttributeUsage(AttributeTargets.Method)] + public sealed class PureAttribute : Attribute { } + + /// + /// Indicates that the return value of the method invocation must be used. + /// + /// + /// Methods decorated with this attribute (in contrast to pure methods) might change state, + /// but make no sense without using their return value.
+ /// Similarly to , this attribute + /// will help detecting usages of the method when the return value in not used. + /// Additionally, you can optionally specify a custom message, which will be used when showing warnings, e.g. + /// [MustUseReturnValue("Use the return value to...")]. + ///
+ [AttributeUsage(AttributeTargets.Method)] + public sealed class MustUseReturnValueAttribute : Attribute + { + public MustUseReturnValueAttribute() { } + + public MustUseReturnValueAttribute([NotNull] string justification) + { + Justification = justification; + } + + [CanBeNull] public string Justification { get; } + } + + /// + /// Indicates the type member or parameter of some type, that should be used instead of all other ways + /// to get the value of that type. This annotation is useful when you have some "context" value evaluated + /// and stored somewhere, meaning that all other ways to get this value must be consolidated with existing one. + /// + /// + /// class Foo { + /// [ProvidesContext] IBarService _barService = ...; + /// + /// void ProcessNode(INode node) { + /// DoSomething(node, node.GetGlobalServices().Bar); + /// // ^ Warning: use value of '_barService' field + /// } + /// } + /// + [AttributeUsage( + AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter | AttributeTargets.Method | + AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Struct | AttributeTargets.GenericParameter)] + public sealed class ProvidesContextAttribute : Attribute { } + + /// + /// Indicates that a parameter is a path to a file or a folder within a web project. + /// Path can be relative or absolute, starting from web root (~). + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class PathReferenceAttribute : Attribute + { + public PathReferenceAttribute() { } + + public PathReferenceAttribute([NotNull, PathReference] string basePath) + { + BasePath = basePath; + } + + [CanBeNull] public string BasePath { get; } + } + + /// + /// An extension method marked with this attribute is processed by code completion + /// as a 'Source Template'. When the extension method is completed over some expression, its source code + /// is automatically expanded like a template at call site. + /// + /// + /// Template method body can contain valid source code and/or special comments starting with '$'. + /// Text inside these comments is added as source code when the template is applied. Template parameters + /// can be used either as additional method parameters or as identifiers wrapped in two '$' signs. + /// Use the attribute to specify macros for parameters. + /// + /// + /// In this example, the 'forEach' method is a source template available over all values + /// of enumerable types, producing ordinary C# 'foreach' statement and placing caret inside block: + /// + /// [SourceTemplate] + /// public static void forEach<T>(this IEnumerable<T> xs) { + /// foreach (var x in xs) { + /// //$ $END$ + /// } + /// } + /// + /// + [AttributeUsage(AttributeTargets.Method)] + public sealed class SourceTemplateAttribute : Attribute { } + + /// + /// Allows specifying a macro for a parameter of a source template. + /// + /// + /// You can apply the attribute on the whole method or on any of its additional parameters. The macro expression + /// is defined in the property. When applied on a method, the target + /// template parameter is defined in the property. To apply the macro silently + /// for the parameter, set the property value = -1. + /// + /// + /// Applying the attribute on a source template method: + /// + /// [SourceTemplate, Macro(Target = "item", Expression = "suggestVariableName()")] + /// public static void forEach<T>(this IEnumerable<T> collection) { + /// foreach (var item in collection) { + /// //$ $END$ + /// } + /// } + /// + /// Applying the attribute on a template method parameter: + /// + /// [SourceTemplate] + /// public static void something(this Entity x, [Macro(Expression = "guid()", Editable = -1)] string newguid) { + /// /*$ var $x$Id = "$newguid$" + x.ToString(); + /// x.DoSomething($x$Id); */ + /// } + /// + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method, AllowMultiple = true)] + public sealed class MacroAttribute : Attribute + { + /// + /// Allows specifying a macro that will be executed for a source template + /// parameter when the template is expanded. + /// + [CanBeNull] public string Expression { get; set; } + + /// + /// Allows specifying which occurrence of the target parameter becomes editable when the template is deployed. + /// + /// + /// If the target parameter is used several times in the template, only one occurrence becomes editable; + /// other occurrences are changed synchronously. To specify the zero-based index of the editable occurrence, + /// use values >= 0. To make the parameter non-editable when the template is expanded, use -1. + /// + public int Editable { get; set; } + + /// + /// Identifies the target parameter of a source template if the + /// is applied on a template method. + /// + [CanBeNull] public string Target { get; set; } + } + + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] + public sealed class AspMvcAreaMasterLocationFormatAttribute : Attribute + { + public AspMvcAreaMasterLocationFormatAttribute([NotNull] string format) + { + Format = format; + } + + [NotNull] public string Format { get; } + } + + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] + public sealed class AspMvcAreaPartialViewLocationFormatAttribute : Attribute + { + public AspMvcAreaPartialViewLocationFormatAttribute([NotNull] string format) + { + Format = format; + } + + [NotNull] public string Format { get; } + } + + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] + public sealed class AspMvcAreaViewLocationFormatAttribute : Attribute + { + public AspMvcAreaViewLocationFormatAttribute([NotNull] string format) + { + Format = format; + } + + [NotNull] public string Format { get; } + } + + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] + public sealed class AspMvcMasterLocationFormatAttribute : Attribute + { + public AspMvcMasterLocationFormatAttribute([NotNull] string format) + { + Format = format; + } + + [NotNull] public string Format { get; } + } + + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] + public sealed class AspMvcPartialViewLocationFormatAttribute : Attribute + { + public AspMvcPartialViewLocationFormatAttribute([NotNull] string format) + { + Format = format; + } + + [NotNull] public string Format { get; } + } + + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] + public sealed class AspMvcViewLocationFormatAttribute : Attribute + { + public AspMvcViewLocationFormatAttribute([NotNull] string format) + { + Format = format; + } + + [NotNull] public string Format { get; } + } + + /// + /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter + /// is an MVC action. If applied to a method, the MVC action name is calculated + /// implicitly from the context. Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Html.ChildActionExtensions.RenderAction(HtmlHelper, String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcActionAttribute : Attribute + { + public AspMvcActionAttribute() { } + + public AspMvcActionAttribute([NotNull] string anonymousProperty) + { + AnonymousProperty = anonymousProperty; + } + + [CanBeNull] public string AnonymousProperty { get; } + } + + /// + /// ASP.NET MVC attribute. Indicates that the marked parameter is an MVC area. + /// Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Html.ChildActionExtensions.RenderAction(HtmlHelper, String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcAreaAttribute : Attribute + { + public AspMvcAreaAttribute() { } + + public AspMvcAreaAttribute([NotNull] string anonymousProperty) + { + AnonymousProperty = anonymousProperty; + } + + [CanBeNull] public string AnonymousProperty { get; } + } + + /// + /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter is + /// an MVC controller. If applied to a method, the MVC controller name is calculated + /// implicitly from the context. Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Html.ChildActionExtensions.RenderAction(HtmlHelper, String, String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcControllerAttribute : Attribute + { + public AspMvcControllerAttribute() { } + + public AspMvcControllerAttribute([NotNull] string anonymousProperty) + { + AnonymousProperty = anonymousProperty; + } + + [CanBeNull] public string AnonymousProperty { get; } + } + + /// + /// ASP.NET MVC attribute. Indicates that the marked parameter is an MVC Master. Use this attribute + /// for custom wrappers similar to System.Web.Mvc.Controller.View(String, String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcMasterAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. Indicates that the marked parameter is an MVC model type. Use this attribute + /// for custom wrappers similar to System.Web.Mvc.Controller.View(String, Object). + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class AspMvcModelTypeAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter is an MVC + /// partial view. If applied to a method, the MVC partial view name is calculated implicitly + /// from the context. Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Html.RenderPartialExtensions.RenderPartial(HtmlHelper, String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcPartialViewAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. Allows disabling inspections for MVC views within a class or a method. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] + public sealed class AspMvcSuppressViewErrorAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. Indicates that a parameter is an MVC display template. + /// Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Html.DisplayExtensions.DisplayForModel(HtmlHelper, String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcDisplayTemplateAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. Indicates that the marked parameter is an MVC editor template. + /// Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Html.EditorExtensions.EditorForModel(HtmlHelper, String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcEditorTemplateAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. Indicates that the marked parameter is an MVC template. + /// Use this attribute for custom wrappers similar to + /// System.ComponentModel.DataAnnotations.UIHintAttribute(System.String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcTemplateAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter + /// is an MVC view component. If applied to a method, the MVC view name is calculated implicitly + /// from the context. Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Controller.View(Object). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcViewAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter + /// is an MVC view component name. + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcViewComponentAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter + /// is an MVC view component view. If applied to a method, the MVC view component view name is default. + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcViewComponentViewAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. When applied to a parameter of an attribute, + /// indicates that this parameter is an MVC action name. + /// + /// + /// [ActionName("Foo")] + /// public ActionResult Login(string returnUrl) { + /// ViewBag.ReturnUrl = Url.Action("Foo"); // OK + /// return RedirectToAction("Bar"); // Error: Cannot resolve action + /// } + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property)] + public sealed class AspMvcActionSelectorAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.Field)] + public sealed class HtmlElementAttributesAttribute : Attribute + { + public HtmlElementAttributesAttribute() { } + + public HtmlElementAttributesAttribute([NotNull] string name) + { + Name = name; + } + + [CanBeNull] public string Name { get; } + } + + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class HtmlAttributeValueAttribute : Attribute + { + public HtmlAttributeValueAttribute([NotNull] string name) + { + Name = name; + } + + [NotNull] public string Name { get; } + } + + /// + /// Razor attribute. Indicates that the marked parameter or method is a Razor section. + /// Use this attribute for custom wrappers similar to + /// System.Web.WebPages.WebPageBase.RenderSection(String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method)] + public sealed class RazorSectionAttribute : Attribute { } + + /// + /// Indicates how method, constructor invocation, or property access + /// over collection type affects the contents of the collection. + /// Use to specify the access type. + /// + /// + /// Using this attribute only makes sense if all collection methods are marked with this attribute. + /// + /// + /// public class MyStringCollection : List<string> + /// { + /// [CollectionAccess(CollectionAccessType.Read)] + /// public string GetFirstString() + /// { + /// return this.ElementAt(0); + /// } + /// } + /// class Test + /// { + /// public void Foo() + /// { + /// // Warning: Contents of the collection is never updated + /// var col = new MyStringCollection(); + /// string x = col.GetFirstString(); + /// } + /// } + /// + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Property)] + public sealed class CollectionAccessAttribute : Attribute + { + public CollectionAccessAttribute(CollectionAccessType collectionAccessType) + { + CollectionAccessType = collectionAccessType; + } + + public CollectionAccessType CollectionAccessType { get; } + } + + /// + /// Provides a value for the to define + /// how the collection method invocation affects the contents of the collection. + /// + [Flags] + public enum CollectionAccessType + { + /// Method does not use or modify content of the collection. + None = 0, + /// Method only reads content of the collection but does not modify it. + Read = 1, + /// Method can change content of the collection but does not add new elements. + ModifyExistingContent = 2, + /// Method can add new elements to the collection. + UpdatedContent = ModifyExistingContent | 4 + } + + /// + /// Indicates that the marked method is assertion method, i.e. it halts the control flow if + /// one of the conditions is satisfied. To set the condition, mark one of the parameters with + /// attribute. + /// + [AttributeUsage(AttributeTargets.Method)] + public sealed class AssertionMethodAttribute : Attribute { } + + /// + /// Indicates the condition parameter of the assertion method. The method itself should be + /// marked by attribute. The mandatory argument of + /// the attribute is the assertion type. + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class AssertionConditionAttribute : Attribute + { + public AssertionConditionAttribute(AssertionConditionType conditionType) + { + ConditionType = conditionType; + } + + public AssertionConditionType ConditionType { get; } + } + + /// + /// Specifies assertion type. If the assertion method argument satisfies the condition, + /// then the execution continues. Otherwise, execution is assumed to be halted. + /// + public enum AssertionConditionType + { + /// Marked parameter should be evaluated to true. + IS_TRUE = 0, + /// Marked parameter should be evaluated to false. + IS_FALSE = 1, + /// Marked parameter should be evaluated to null value. + IS_NULL = 2, + /// Marked parameter should be evaluated to not null value. + IS_NOT_NULL = 3, + } + + /// + /// Indicates that the marked method unconditionally terminates control flow execution. + /// For example, it could unconditionally throw exception. + /// + [Obsolete("Use [ContractAnnotation('=> halt')] instead")] + [AttributeUsage(AttributeTargets.Method)] + public sealed class TerminatesProgramAttribute : Attribute { } + + /// + /// Indicates that method is pure LINQ method, with postponed enumeration (like Enumerable.Select, + /// .Where). This annotation allows inference of [InstantHandle] annotation for parameters + /// of delegate type by analyzing LINQ method chains. + /// + [AttributeUsage(AttributeTargets.Method)] + public sealed class LinqTunnelAttribute : Attribute { } + + /// + /// Indicates that IEnumerable passed as a parameter is not enumerated. + /// Use this annotation to suppress the 'Possible multiple enumeration of IEnumerable' inspection. + /// + /// + /// static void ThrowIfNull<T>([NoEnumeration] T v, string n) where T : class + /// { + /// // custom check for null but no enumeration + /// } + /// + /// void Foo(IEnumerable<string> values) + /// { + /// ThrowIfNull(values, nameof(values)); + /// var x = values.ToList(); // No warnings about multiple enumeration + /// } + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class NoEnumerationAttribute : Attribute { } + + /// + /// Indicates that the marked parameter is a regular expression pattern. + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class RegexPatternAttribute : Attribute { } + + /// + /// Prevents the Member Reordering feature from tossing members of the marked class. + /// + /// + /// The attribute must be mentioned in your member reordering patterns. + /// + [AttributeUsage( + AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Struct | AttributeTargets.Enum)] + public sealed class NoReorderAttribute : Attribute { } + + /// + /// XAML attribute. Indicates the type that has ItemsSource property and should be treated + /// as ItemsControl-derived type, to enable inner items DataContext type resolve. + /// + [AttributeUsage(AttributeTargets.Class)] + public sealed class XamlItemsControlAttribute : Attribute { } + + /// + /// XAML attribute. Indicates the property of some BindingBase-derived type, that + /// is used to bind some item of ItemsControl-derived type. This annotation will + /// enable the DataContext type resolve for XAML bindings for such properties. + /// + /// + /// Property should have the tree ancestor of the ItemsControl type or + /// marked with the attribute. + /// + [AttributeUsage(AttributeTargets.Property)] + public sealed class XamlItemBindingOfItemsControlAttribute : Attribute { } + + /// + /// XAML attribute. Indicates the property of some Style-derived type, that + /// is used to style items of ItemsControl-derived type. This annotation will + /// enable the DataContext type resolve for XAML bindings for such properties. + /// + /// + /// Property should have the tree ancestor of the ItemsControl type or + /// marked with the attribute. + /// + [AttributeUsage(AttributeTargets.Property)] + public sealed class XamlItemStyleOfItemsControlAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + public sealed class AspChildControlTypeAttribute : Attribute + { + public AspChildControlTypeAttribute([NotNull] string tagName, [NotNull] Type controlType) + { + TagName = tagName; + ControlType = controlType; + } + + [NotNull] public string TagName { get; } + + [NotNull] public Type ControlType { get; } + } + + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method)] + public sealed class AspDataFieldAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method)] + public sealed class AspDataFieldsAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Property)] + public sealed class AspMethodPropertyAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + public sealed class AspRequiredAttributeAttribute : Attribute + { + public AspRequiredAttributeAttribute([NotNull] string attribute) + { + Attribute = attribute; + } + + [NotNull] public string Attribute { get; } + } + + [AttributeUsage(AttributeTargets.Property)] + public sealed class AspTypePropertyAttribute : Attribute + { + public bool CreateConstructorReferences { get; } + + public AspTypePropertyAttribute(bool createConstructorReferences) + { + CreateConstructorReferences = createConstructorReferences; + } + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class RazorImportNamespaceAttribute : Attribute + { + public RazorImportNamespaceAttribute([NotNull] string name) + { + Name = name; + } + + [NotNull] public string Name { get; } + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class RazorInjectionAttribute : Attribute + { + public RazorInjectionAttribute([NotNull] string type, [NotNull] string fieldName) + { + Type = type; + FieldName = fieldName; + } + + [NotNull] public string Type { get; } + + [NotNull] public string FieldName { get; } + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class RazorDirectiveAttribute : Attribute + { + public RazorDirectiveAttribute([NotNull] string directive) + { + Directive = directive; + } + + [NotNull] public string Directive { get; } + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class RazorPageBaseTypeAttribute : Attribute + { + public RazorPageBaseTypeAttribute([NotNull] string baseType) + { + BaseType = baseType; + } + public RazorPageBaseTypeAttribute([NotNull] string baseType, string pageName) + { + BaseType = baseType; + PageName = pageName; + } + + [NotNull] public string BaseType { get; } + [CanBeNull] public string PageName { get; } + } + + [AttributeUsage(AttributeTargets.Method)] + public sealed class RazorHelperCommonAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Property)] + public sealed class RazorLayoutAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Method)] + public sealed class RazorWriteLiteralMethodAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Method)] + public sealed class RazorWriteMethodAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class RazorWriteMethodParameterAttribute : Attribute { } +} \ No newline at end of file diff --git a/src/ExceptionReporter.WPF/Properties/AssemblyInfo.cs b/src/ExceptionReporter.WPF/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..9f396aaf --- /dev/null +++ b/src/ExceptionReporter.WPF/Properties/AssemblyInfo.cs @@ -0,0 +1,55 @@ +using System.Reflection; +using System.Resources; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Windows; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("ExceptionReporter.WPF")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("WPF ExceptionReporter")] +[assembly: AssemblyCopyright("Copyright © 2021")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +//In order to begin building localizable applications, set +//CultureYouAreCodingWith in your .csproj file +//inside a . For example, if you are using US english +//in your source files, set the to en-US. Then uncomment +//the NeutralResourceLanguage attribute below. Update the "en-US" in +//the line below to match the UICulture setting in the project file. + +//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)] + + +[assembly:ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] + + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/ExceptionReporter.WPF/Properties/Settings.Designer.cs b/src/ExceptionReporter.WPF/Properties/Settings.Designer.cs new file mode 100644 index 00000000..17d1bbad --- /dev/null +++ b/src/ExceptionReporter.WPF/Properties/Settings.Designer.cs @@ -0,0 +1,26 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace ExceptionReporting.Properties { + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.10.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default { + get { + return defaultInstance; + } + } + } +} diff --git a/src/ExceptionReporter.WPF/Properties/Settings.settings b/src/ExceptionReporter.WPF/Properties/Settings.settings new file mode 100644 index 00000000..033d7a5e --- /dev/null +++ b/src/ExceptionReporter.WPF/Properties/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/ExceptionReporting/ExceptionReporter.NET.csproj b/src/ExceptionReporter.WinForms/ExceptionReporter.WinForms.csproj similarity index 72% rename from src/ExceptionReporting/ExceptionReporter.NET.csproj rename to src/ExceptionReporter.WinForms/ExceptionReporter.WinForms.csproj index 3f5ef7b9..86d571d1 100644 --- a/src/ExceptionReporting/ExceptionReporter.NET.csproj +++ b/src/ExceptionReporter.WinForms/ExceptionReporter.WinForms.csproj @@ -1,4 +1,4 @@ - + ExceptionReporting ExceptionReporter.NET @@ -9,22 +9,24 @@ https://raw.githubusercontent.com/PandaWood/ExceptionReporter.NET/master/LICENSE.txt https://github.com/PandaWood/ExceptionReporter.NET + + + + - - - - - - - - - - - + + + Designer + ResXFileCodeGenerator + + + Designer + ResXFileCodeGenerator + @@ -33,30 +35,29 @@ + + 4.1.0 + 4.1.0.0 + Peter van der Woude, Simon Cropp + ExceptionReporter is a .NET component that gathers detailed information on an Exception and the application/system running it. It allows the user to copy, save or email a report to the developer + + + + + True + + Resources.resx True True - Resources.resx - - ResXFileCodeGenerator - - ResXFileCodeGenerator + Designer Resources.Designer.cs - - ResXFileCodeGenerator - - 4.1.0 - 4.1.0.0 - Peter van der Woude, Simon Cropp - ExceptionReporter is a .NET component that gathers detailed information on an Exception and the application/system running it. It allows the user to copy, save or email a report to the developer - - \ No newline at end of file diff --git a/src/ExceptionReporting/MVP/Presenters/ExceptionReportPresenter.cs b/src/ExceptionReporter.WinForms/MVP/Presenters/ExceptionReportPresenter.cs similarity index 60% rename from src/ExceptionReporting/MVP/Presenters/ExceptionReportPresenter.cs rename to src/ExceptionReporter.WinForms/MVP/Presenters/ExceptionReportPresenter.cs index 8d9e3dee..ba19f51f 100644 --- a/src/ExceptionReporting/MVP/Presenters/ExceptionReportPresenter.cs +++ b/src/ExceptionReporter.WinForms/MVP/Presenters/ExceptionReportPresenter.cs @@ -1,15 +1,14 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using ExceptionReporting.Core; using ExceptionReporting.Mail; -using ExceptionReporting.MVP.Views; using ExceptionReporting.Network; using ExceptionReporting.Properties; using ExceptionReporting.Report; using ExceptionReporting.SystemInfo; -using ExceptionReporting.Templates; +using ExceptionReporting.Zip; +using ExceptionReporting.WinForms; namespace ExceptionReporting.MVP.Presenters { @@ -18,8 +17,8 @@ namespace ExceptionReporting.MVP.Presenters /// internal class ExceptionReportPresenter { - private readonly IFileService _fileService; private readonly ReportGenerator _reportGenerator; + private readonly ReportFileZipper _reportFileZipper; /// /// constructor accepting a view and the data/config of the report @@ -27,7 +26,7 @@ internal class ExceptionReportPresenter public ExceptionReportPresenter(IExceptionReportView view, ExceptionReportInfo info) { _reportGenerator = new ReportGenerator(info); - _fileService = new FileService(); + _reportFileZipper = new ReportFileZipper(new FileService(), _reportGenerator, info); View = view; ReportInfo = info; } @@ -44,23 +43,6 @@ private string CreateReport() return _reportGenerator.Generate(); } - /// - /// Save the exception report to file/disk - /// - /// the filename to save to - public void SaveTextReportToFile(string fileName) - { - if (string.IsNullOrEmpty(fileName)) return; - - var report = CreateReport(); - var result = _fileService.Write(fileName, report); - - if (!result.Saved) - { - View.ShowError(Resources.Unable_to_save_file + $" '{fileName}'", result.Exception); - } - } - /// /// Save the exception report to file/disk /// @@ -68,25 +50,10 @@ public void SaveTextReportToFile(string fileName) public void SaveZipReportToFile(string zipFilePath) { if (string.IsNullOrEmpty(zipFilePath)) return; - - //TODO: select extension by ReportTemplateFormat - var textReportPath = Path.Combine(Path.GetTempPath(), @"ExceptionReporter" + Path.DirectorySeparatorChar + "report.txt"); - if (!Directory.Exists(textReportPath)) Directory.CreateDirectory(Path.GetDirectoryName(textReportPath)); - var report = CreateReport(); - var textFileSaveResult = _fileService.Write(textReportPath, report); - if (!textFileSaveResult.Saved) - { - View.ShowError(Resources.Unable_to_save_file + $" '{textReportPath}'", textFileSaveResult.Exception); - } - else + var result = _reportFileZipper.Save(zipFilePath); + if (!result.Saved) { - var additionalFilesToAttach = new List{ textReportPath }; - var zipReport = new ZipReportService(new Zipper(), new ScreenshotTaker(), new FileService()); - var result = zipReport.CreateZipReport(ReportInfo, zipFilePath, additionalFilesToAttach); - if (!File.Exists(result)) - { - View.ShowError(Resources.Unable_to_save_file + $" '{result}'", new IOException()); - } + View.ShowError(Resources.Unable_to_save_file + $"'{result}'", result.Exception); } } @@ -98,16 +65,16 @@ public void SendReport() View.EnableEmailButton = false; View.ShowProgressBar = true; - var sender = new SenderFactory(ReportInfo, View).Get(); + var sender = new SenderFactory(ReportInfo, View, new WinFormsScreenShooter()).Get(); View.ProgressMessage = sender.ConnectingMessage; try { - var report = ReportInfo.IsSimpleMAPI() ? CreateEmailReport() : CreateReport(); + var report = ReportInfo.IsSimpleMAPI() ? new EmailReporter(ReportInfo).Create() : CreateReport(); sender.Send(report); } catch (Exception exception) - { // most exceptions will be thrown in the Sender - this is just a backup + { View.Completed(false); View.ShowError(Resources.Unable_to_setup + $" {sender.Description}" + Environment.NewLine + exception.Message, exception); @@ -139,18 +106,6 @@ public void ToggleDetail() View.ToggleShowFullDetail(); } - private string CreateEmailReport() - { - var template = new TemplateRenderer(new EmailIntroModel - { - ScreenshotTaken = ReportInfo.TakeScreenshot - }); - var emailIntro = template.RenderPreset(); - var report = CreateReport(); - - return emailIntro + report; - } - /// /// Fetch the WMI system information /// @@ -170,8 +125,7 @@ public void PopulateReport() View.PopulateExceptionTab(ReportInfo.Exceptions); View.PopulateAssembliesTab(); - if (ExceptionReporter.NotRunningMono()) - View.PopulateSysInfoTab(); + View.PopulateSysInfoTab(); } finally { diff --git a/src/ExceptionReporting/MVP/Views/ExceptionDetailControl.Designer.cs b/src/ExceptionReporter.WinForms/MVP/Views/ExceptionDetailControl.Designer.cs similarity index 100% rename from src/ExceptionReporting/MVP/Views/ExceptionDetailControl.Designer.cs rename to src/ExceptionReporter.WinForms/MVP/Views/ExceptionDetailControl.Designer.cs diff --git a/src/ExceptionReporting/MVP/Views/ExceptionDetailControl.cs b/src/ExceptionReporter.WinForms/MVP/Views/ExceptionDetailControl.cs similarity index 94% rename from src/ExceptionReporting/MVP/Views/ExceptionDetailControl.cs rename to src/ExceptionReporter.WinForms/MVP/Views/ExceptionDetailControl.cs index f69e1f1a..aa317240 100644 --- a/src/ExceptionReporting/MVP/Views/ExceptionDetailControl.cs +++ b/src/ExceptionReporter.WinForms/MVP/Views/ExceptionDetailControl.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Drawing; using System.Windows.Forms; @@ -13,11 +13,11 @@ public ExceptionDetailControl() WireUpEvents(); } - public void SetControlBackgrounds(Color color) + public void SetControlBackgrounds(string color) { listviewExceptions.BackColor = txtExceptionTabMessage.BackColor = - txtExceptionTabStackTrace.BackColor = color; + txtExceptionTabStackTrace.BackColor = ColorTranslator.FromHtml(color); } //TODO this is duplicated almost entirely (without ListView) in ReportBuilder) @@ -49,7 +49,7 @@ public void PopulateExceptionTab(Exception rootException) currentException = currentException.InnerException; listViewItem = new ListViewItem { - Text = string.Format("Inner Exception {0}", index) + Text = $"Inner Exception {index}" }; listViewItem.SubItems.Add(currentException.GetType().ToString()); AddTargetSite(listViewItem, currentException); diff --git a/src/ExceptionReporting/MVP/Views/ExceptionDetailControl.en.resx b/src/ExceptionReporter.WinForms/MVP/Views/ExceptionDetailControl.en.resx similarity index 100% rename from src/ExceptionReporting/MVP/Views/ExceptionDetailControl.en.resx rename to src/ExceptionReporter.WinForms/MVP/Views/ExceptionDetailControl.en.resx diff --git a/src/ExceptionReporting/MVP/Views/ExceptionDetailControl.resx b/src/ExceptionReporter.WinForms/MVP/Views/ExceptionDetailControl.resx similarity index 100% rename from src/ExceptionReporting/MVP/Views/ExceptionDetailControl.resx rename to src/ExceptionReporter.WinForms/MVP/Views/ExceptionDetailControl.resx diff --git a/src/ExceptionReporting/MVP/Views/ExceptionDetailControl.ru.resx b/src/ExceptionReporter.WinForms/MVP/Views/ExceptionDetailControl.ru.resx similarity index 100% rename from src/ExceptionReporting/MVP/Views/ExceptionDetailControl.ru.resx rename to src/ExceptionReporter.WinForms/MVP/Views/ExceptionDetailControl.ru.resx diff --git a/src/ExceptionReporting/MVP/Views/ExceptionReportView.Designer.cs b/src/ExceptionReporter.WinForms/MVP/Views/ExceptionReportView.Designer.cs similarity index 100% rename from src/ExceptionReporting/MVP/Views/ExceptionReportView.Designer.cs rename to src/ExceptionReporter.WinForms/MVP/Views/ExceptionReportView.Designer.cs diff --git a/src/ExceptionReporting/MVP/Views/ExceptionReportView.cs b/src/ExceptionReporter.WinForms/MVP/Views/ExceptionReportView.cs similarity index 90% rename from src/ExceptionReporting/MVP/Views/ExceptionReportView.cs rename to src/ExceptionReporter.WinForms/MVP/Views/ExceptionReportView.cs index bb699c71..d8c5086d 100644 --- a/src/ExceptionReporting/MVP/Views/ExceptionReportView.cs +++ b/src/ExceptionReporter.WinForms/MVP/Views/ExceptionReportView.cs @@ -1,3 +1,7 @@ +/* + * https://github.com/PandaWood/ExceptionReporter.NET + */ + using System; using System.Collections.Generic; using System.Drawing; @@ -6,7 +10,7 @@ using ExceptionReporting.Core; using ExceptionReporting.MVP.Presenters; using ExceptionReporting.Properties; -using ExceptionReporting.SystemInfo; +using ExceptionReporting.WinForms; #pragma warning disable 1591 @@ -35,7 +39,8 @@ public ExceptionReportView(ExceptionReportInfo reportInfo) private void PopulateReportInfo(ExceptionReportInfo reportInfo) { - lblExplanation.Text = reportInfo.UserExplanationLabel; + lblExplanation.Text = reportInfo.UserExplanationLabel ?? + Resources.Please_enter_a_brief_explanation_of_events_leading_up_to_this_exception; ShowFullDetail = reportInfo.ShowFullDetail; ToggleShowFullDetail(); btnDetailToggle.Visible = reportInfo.ShowLessDetailButton; @@ -68,7 +73,7 @@ private void PopulateReportInfo(ExceptionReportInfo reportInfo) txtApplicationName.BackColor = txtDate.BackColor = txtExceptionMessageLarge.BackColor = - txtExceptionMessage.BackColor = reportInfo.BackgroundColor; + txtExceptionMessage.BackColor = ColorTranslator.FromHtml("#FFCC66"); if (!reportInfo.ShowButtonIcons) { @@ -80,17 +85,14 @@ private void PopulateReportInfo(ExceptionReportInfo reportInfo) RemoveEmailButton(); } - Text = reportInfo.TitleText; + Text = reportInfo.TitleText ?? Resources.ErrorReport; txtUserExplanation.Font = new Font(txtUserExplanation.Font.FontFamily, reportInfo.UserExplanationFontSize); lblContactCompany.Text = string.Format( Resources.ExceptionReportView_PopulateReportInfo_If_this_problem_persists__please_contact__0__support_ + Environment.NewLine, reportInfo.CompanyName); - btnSimpleEmail.Text = string.Format("{0} {1}", - reportInfo.SendMethod == ReportSendMethod.WebService ? Resources.Send : "Email", - reportInfo.SendMethod == ReportSendMethod.WebService && !reportInfo.CompanyName.IsEmpty() - ? string.Format(Resources.to__0_, reportInfo.CompanyName) - : reportInfo.CompanyName); + btnSimpleEmail.Text = + $"{(reportInfo.SendMethod == ReportSendMethod.WebService ? Resources.Send : "Email")} {(reportInfo.SendMethod == ReportSendMethod.WebService && !reportInfo.CompanyName.IsEmpty() ? string.Format(Resources.to__0_, reportInfo.CompanyName) : reportInfo.CompanyName)}"; btnEmail.Text = reportInfo.SendMethod == ReportSendMethod.WebService ? Resources.Send : "Email"; } @@ -153,17 +155,17 @@ public string ProgressMessage public bool EnableEmailButton { - set { btnEmail.Enabled = value; } + set => btnEmail.Enabled = value; } public bool ShowProgressBar { - set { progressBar.Visible = value; } + set => progressBar.Visible = value; } private bool ShowProgressLabel { - set { lblProgressMessage.Visible = value; } + set => lblProgressMessage.Visible = value; } public bool ShowFullDetail { get; set; } @@ -194,10 +196,7 @@ protected override void OnSizeChanged(EventArgs e) FormBorderStyle = ShowFullDetail ? FormBorderStyle.Sizable : FormBorderStyle.FixedDialog; } - public string UserExplanation - { - get { return txtUserExplanation.Text; } - } + public string UserExplanation => txtUserExplanation.Text; public void Completed(bool success) { @@ -282,7 +281,7 @@ public void PopulateExceptionTab(IEnumerable exceptions) for (var index = 0; index < exs.Length; index++) { var exception = exs[index]; - var tabPage = new TabPage { Text = string.Format("Exception {0}", index + 1) }; + var tabPage = new TabPage { Text = $"Exception {index + 1}"}; innerTabControl.TabPages.Add(tabPage); AddExceptionControl(tabPage, exception); } @@ -323,7 +322,7 @@ private TreeNode CreateSysInfoTree() foreach (var sysInfoResult in _presenter.GetSysInfoResults()) { - SysInfoResultMapper.AddTreeViewNode(rootNode, sysInfoResult); + SysInfoResultMapperWinForm.AddTreeViewNode(rootNode, sysInfoResult); } return rootNode; diff --git a/src/ExceptionReporting/MVP/Views/ExceptionReportView.en.resx b/src/ExceptionReporter.WinForms/MVP/Views/ExceptionReportView.en.resx similarity index 100% rename from src/ExceptionReporting/MVP/Views/ExceptionReportView.en.resx rename to src/ExceptionReporter.WinForms/MVP/Views/ExceptionReportView.en.resx diff --git a/src/ExceptionReporting/MVP/Views/ExceptionReportView.resx b/src/ExceptionReporter.WinForms/MVP/Views/ExceptionReportView.resx similarity index 100% rename from src/ExceptionReporting/MVP/Views/ExceptionReportView.resx rename to src/ExceptionReporter.WinForms/MVP/Views/ExceptionReportView.resx diff --git a/src/ExceptionReporting/MVP/Views/ExceptionReportView.ru.resx b/src/ExceptionReporter.WinForms/MVP/Views/ExceptionReportView.ru.resx similarity index 100% rename from src/ExceptionReporting/MVP/Views/ExceptionReportView.ru.resx rename to src/ExceptionReporter.WinForms/MVP/Views/ExceptionReportView.ru.resx diff --git a/src/ExceptionReporting/Properties/Resources.Designer.cs b/src/ExceptionReporter.WinForms/Properties/Resources.Designer.cs similarity index 99% rename from src/ExceptionReporting/Properties/Resources.Designer.cs rename to src/ExceptionReporter.WinForms/Properties/Resources.Designer.cs index 612909aa..5f962c88 100644 --- a/src/ExceptionReporting/Properties/Resources.Designer.cs +++ b/src/ExceptionReporter.WinForms/Properties/Resources.Designer.cs @@ -70,7 +70,7 @@ internal static string Copied_to_clipboard { } /// - /// Looks up a localized string similar to ErrorReport. + /// Looks up a localized string similar to Error Report. /// internal static string ErrorReport { get { diff --git a/src/ExceptionReporting/Properties/Resources.en.resx b/src/ExceptionReporter.WinForms/Properties/Resources.en.resx similarity index 90% rename from src/ExceptionReporting/Properties/Resources.en.resx rename to src/ExceptionReporter.WinForms/Properties/Resources.en.resx index c930a5a4..a1fbbe92 100644 --- a/src/ExceptionReporting/Properties/Resources.en.resx +++ b/src/ExceptionReporter.WinForms/Properties/Resources.en.resx @@ -121,51 +121,51 @@ Test Loc - Please enter a brief explanation of events leading up to this exception + Please enter a brief explanation of events leading up to this exception - The following details can be used to obtain support for this application + The following details can be used to obtain support for this application - Send + Send - to {0} + to {0} - Less Detail + Less Detail - More Detail + More Detail - Report sent + Report sent - Failed to send report + Failed to send report - Error sending report + Error sending report - Failed trying to report an Error + Failed trying to report an Error - Copied to clipboard + Copied to clipboard - Unable to save file + Unable to save file - Unable to setup + Unable to setup - If this problem persists, please contact {0} support. + If this problem persists, please contact {0} support. - ErrorReport + Error Report - Clipboard copy failed + Clipboard copy failed \ No newline at end of file diff --git a/src/ExceptionReporting/Properties/Resources.resx b/src/ExceptionReporter.WinForms/Properties/Resources.resx similarity index 99% rename from src/ExceptionReporting/Properties/Resources.resx rename to src/ExceptionReporter.WinForms/Properties/Resources.resx index e908e3c6..e00b36b8 100644 --- a/src/ExceptionReporting/Properties/Resources.resx +++ b/src/ExceptionReporter.WinForms/Properties/Resources.resx @@ -166,6 +166,6 @@ Unable to setup - ErrorReport + Error Report \ No newline at end of file diff --git a/src/ExceptionReporting/Properties/Resources.ru.resx b/src/ExceptionReporter.WinForms/Properties/Resources.ru.resx similarity index 100% rename from src/ExceptionReporting/Properties/Resources.ru.resx rename to src/ExceptionReporter.WinForms/Properties/Resources.ru.resx diff --git a/src/ExceptionReporter.WinForms/WinForms/DefaultWinFormsViewmaker.cs b/src/ExceptionReporter.WinForms/WinForms/DefaultWinFormsViewmaker.cs new file mode 100644 index 00000000..dc1cb681 --- /dev/null +++ b/src/ExceptionReporter.WinForms/WinForms/DefaultWinFormsViewmaker.cs @@ -0,0 +1,33 @@ +using System.Windows.Forms; +using ExceptionReporting.Core; +using ExceptionReporting.MVP.Views; +using ExceptionReporting.Properties; + +// ReSharper disable MemberCanBePrivate.Global +// ReSharper disable UnusedAutoPropertyAccessor.Global + +namespace ExceptionReporting.WinForms +{ + /// + /// Default/WinForms implementation of IViewmaker + /// + internal class DefaultWinFormsViewmaker : IViewMaker + { + private readonly ExceptionReportInfo _reportInfo; + + public DefaultWinFormsViewmaker(ExceptionReportInfo reportInfo) + { + _reportInfo = reportInfo; + } + + public IExceptionReportView Create() + { + return new ExceptionReportView(_reportInfo); + } + + public void ShowError(string message) + { + MessageBox.Show(message, Resources.Failed_trying_to_report_an_Error, MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } +} \ No newline at end of file diff --git a/src/ExceptionReporter.WinForms/WinForms/SysInfoResultMapperWinForm.cs b/src/ExceptionReporter.WinForms/WinForms/SysInfoResultMapperWinForm.cs new file mode 100644 index 00000000..49a084f5 --- /dev/null +++ b/src/ExceptionReporter.WinForms/WinForms/SysInfoResultMapperWinForm.cs @@ -0,0 +1,36 @@ +/* + * https://github.com/PandaWood/ExceptionReporter.NET + */ + +using System.Linq; +using System.Windows.Forms; +using ExceptionReporting.SystemInfo; + +namespace ExceptionReporting.WinForms +{ + /// + /// WinForms extension + /// + // ReSharper disable once ClassNeverInstantiated.Global + internal class SysInfoResultMapperWinForm : SysInfoResultMapper + { + /// + /// Add a tree node to an existing parentNode, by passing the SysInfoResult + /// + public static void AddTreeViewNode(TreeNode parentNode, SysInfoResult result) + { + var nodeRoot = new TreeNode(result.Name); + + foreach (var nodeLeaf in result.Nodes.Select(nodeValueParent => new TreeNode(nodeValueParent))) + { + nodeRoot.Nodes.Add(nodeLeaf); + + foreach (var nodeValue in result.ChildResults.SelectMany(childResult => childResult.Nodes)) + { + nodeLeaf.Nodes.Add(new TreeNode(nodeValue)); + } + } + parentNode.Nodes.Add(nodeRoot); + } + } +} \ No newline at end of file diff --git a/src/ExceptionReporting/Core/ScreenshotTaker.cs b/src/ExceptionReporter.WinForms/WinForms/WinFormsScreenShooter.cs similarity index 86% rename from src/ExceptionReporting/Core/ScreenshotTaker.cs rename to src/ExceptionReporter.WinForms/WinForms/WinFormsScreenShooter.cs index f094e1b3..8356a55f 100644 --- a/src/ExceptionReporting/Core/ScreenshotTaker.cs +++ b/src/ExceptionReporter.WinForms/WinForms/WinFormsScreenShooter.cs @@ -2,18 +2,14 @@ using System.Drawing.Imaging; using System.IO; using System.Windows.Forms; +using ExceptionReporting.Core; -namespace ExceptionReporting.Core +namespace ExceptionReporting.WinForms { - internal interface IScreenshotTaker - { - string TakeScreenShot(); - } - /// /// Utility to take a screenshot and return as a graphic file /// - internal class ScreenshotTaker : IScreenshotTaker + internal class WinFormsScreenShooter : IScreenShooter { private const string ScreenshotFileName = "exceptionreport-screenshot.jpg"; @@ -21,8 +17,6 @@ internal class ScreenshotTaker : IScreenshotTaker /// temp file name of JPEG image public string TakeScreenShot() { - if (ExceptionReporter.IsRunningMono()) return null; - var rectangle = Rectangle.Empty; foreach (var screen in Screen.AllScreens) diff --git a/src/ExceptionReporting/Core/WinformsClipboard.cs b/src/ExceptionReporter.WinForms/WinForms/WinformsClipboard.cs similarity index 91% rename from src/ExceptionReporting/Core/WinformsClipboard.cs rename to src/ExceptionReporter.WinForms/WinForms/WinformsClipboard.cs index a353964e..a542d27b 100644 --- a/src/ExceptionReporting/Core/WinformsClipboard.cs +++ b/src/ExceptionReporter.WinForms/WinForms/WinformsClipboard.cs @@ -1,6 +1,6 @@ using System; -namespace ExceptionReporting.Core +namespace ExceptionReporting.WinForms { internal static class WinFormsClipboard { diff --git a/src/ExceptionReporting/ExceptionReporter.cs b/src/ExceptionReporter.WinForms/WinFormsExceptionReporter.cs similarity index 50% rename from src/ExceptionReporting/ExceptionReporter.cs rename to src/ExceptionReporter.WinForms/WinFormsExceptionReporter.cs index 1c51bc90..5b41d3fe 100644 --- a/src/ExceptionReporting/ExceptionReporter.cs +++ b/src/ExceptionReporter.WinForms/WinFormsExceptionReporter.cs @@ -1,51 +1,34 @@ -/* - * https://github.com/PandaWood/ExceptionReporter.NET - */ - using System; -using System.Runtime.CompilerServices; -using ExceptionReporting.MVP.Views; -using ExceptionReporting.Network; +using System.Diagnostics; using ExceptionReporting.Network.Events; -using ExceptionReporting.Properties; +using ExceptionReporting.WinForms; -// ReSharper disable MemberCanBePrivate.Global // ReSharper disable UnusedMember.Global - -[assembly: InternalsVisibleTo("Tests.ExceptionReporter.NET")] -[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // for moq +// ReSharper disable ConvertToAutoPropertyWhenPossible +// ReSharper disable MemberCanBePrivate.Global +// ReSharper disable UnusedType.Global namespace ExceptionReporting { /// - /// The entry-point (class) to invoking an ExceptionReporter dialog + /// The entry-point (class) to invoking a (WinForms) ExceptionReporter dialog /// eg new ExceptionReporter().Show(exceptions) /// - public class ExceptionReporter + public class ExceptionReporter : ExceptionReporterBase { - private readonly ExceptionReportInfo _info; - - /// - /// Contract by which to show any dialogs/view - /// - public IViewMaker ViewMaker { get; set; } - /// /// Initialise the ExceptionReporter /// public ExceptionReporter() { - _info = new ExceptionReportInfo(); - ViewMaker = new ViewMaker(_info); + ViewMaker = new DefaultWinFormsViewmaker(_info); } /// - /// Public access to configuration/settings + /// Contract by which to show any dialogs/view /// - public ExceptionReportInfo Config - { - get { return _info; } - } + // ReSharper disable once UnusedAutoPropertyAccessor.Global + public IViewMaker ViewMaker { get; set; } /// /// Show the ExceptionReport dialog @@ -57,18 +40,23 @@ public bool Show(params Exception[] exceptions) { // silently ignore the mistake of passing null if (exceptions == null || exceptions.Length == 0 || exceptions.Length >= 1 && exceptions[0] == null) return false; + if (ViewMaker == null) + { + Debug.WriteLine("DefaultWinFormsViewmaker must be initialized (not null). Add `er.DefaultWinFormsViewmaker = new DefaultWinFormsViewmaker(er.Config);` where 'er' is the ExceptionReporter object"); + return false; + } try { _info.SetExceptions(exceptions); - + var view = ViewMaker.Create(); view.ShowWindow(); return true; } catch (Exception ex) { - ViewMaker.ShowError(ex.Message, Resources.Failed_trying_to_report_an_Error); + ViewMaker.ShowError(ex.Message); return false; } } @@ -84,21 +72,6 @@ public void Show(string customMessage, params Exception[] exceptions) Show(exceptions); } - /// - /// Send the report, asynchronously, without showing a dialog (silent send) - /// must be SMTP or WebService, else this is ignored (silently) - /// - /// Provide implementation of IReportSendEvent to receive error/updates on calling thread - /// The exception/s to include in the report - public void Send(IReportSendEvent sendEvent = null, params Exception[] exceptions) - { - _info.SetExceptions(exceptions); - - var sender = new SenderFactory(_info, sendEvent ?? new SilentSendEvent()).Get(); - var report = new ReportGenerator(_info); - sender.Send(report.Generate()); - } - /// /// Send the report, asynchronously, without showing a dialog (silent send) /// must be SMTP or WebService, else this is ignored (silently) @@ -106,17 +79,7 @@ public void Send(IReportSendEvent sendEvent = null, params Exception[] exception /// The exception/s to include in the report public void Send(params Exception[] exceptions) { - Send(new SilentSendEvent(), exceptions); + base.Send(new WinFormsScreenShooter(), new SilentSendEvent(), exceptions); } - - static readonly bool _isRunningMono = System.Type.GetType("Mono.Runtime") != null; - - /// true, if running mono false otherwise. - public static bool IsRunningMono() { return _isRunningMono; } - - /// true, if not running mono false otherwise. - public static bool NotRunningMono() { return !_isRunningMono; } } } - -// ReSharper restore UnusedMember.Global diff --git a/src/ExceptionReporting/MVP/Views/ViewMaker.cs b/src/ExceptionReporting/MVP/Views/ViewMaker.cs deleted file mode 100644 index e94f8787..00000000 --- a/src/ExceptionReporting/MVP/Views/ViewMaker.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Windows.Forms; -using ExceptionReporting.MVP.Views; - -// ReSharper disable MemberCanBePrivate.Global -// ReSharper disable UnusedAutoPropertyAccessor.Global - -/// -/// contract to show view-related things -/// -public interface IViewMaker -{ - /// - /// create the main view/dialog - /// - /// - IExceptionReportView Create(); - - /// - /// show error - /// - /// - /// - void ShowError(string message, string description); -} - -namespace ExceptionReporting.MVP.Views -{ - internal class ViewMaker : IViewMaker - { - private readonly ExceptionReportInfo _reportInfo; - - public ViewMaker(ExceptionReportInfo reportInfo) - { - _reportInfo = reportInfo; - } - - public IExceptionReportView Create() - { - return new ExceptionReportView(_reportInfo); - } - - public void ShowError(string message, string description) - { - MessageBox.Show(message, description, MessageBoxButtons.OK, MessageBoxIcon.Error); - } - } -} \ No newline at end of file diff --git a/src/Tests/Attacher_Tests.cs b/src/Tests/Attacher_Tests.cs index 3ff773c0..3189fb37 100644 --- a/src/Tests/Attacher_Tests.cs +++ b/src/Tests/Attacher_Tests.cs @@ -1,6 +1,7 @@ using ExceptionReporting; using ExceptionReporting.Core; using ExceptionReporting.Mail; +using ExceptionReporting.Zip; using Moq; using NUnit.Framework; @@ -23,7 +24,7 @@ public void SetUp() [Test] public void Can_Attach_Nothing_If_None_To_Attach() { - var attacher = new Attacher(new ExceptionReportInfo { FilesToAttach = new string[] {} }) + var attacher = new Attacher(new ExceptionReportInfo { FilesToAttach = new string[] {} }, new Mock().Object) { FileService = _file.Object, Zipper = _zip.Object @@ -40,7 +41,7 @@ public void Can_Attach_1_With_1_File() _file.Setup(f => f.Exists("file1")).Returns(true); _file.Setup(f => f.TempFile(It.IsAny())).Returns("attach.zip"); - var attacher = new Attacher(new ExceptionReportInfo { FilesToAttach = new[] { "file1" } }) + var attacher = new Attacher(new ExceptionReportInfo { FilesToAttach = new[] { "file1" } }, new Mock().Object) { FileService = _file.Object, Zipper = _zip.Object @@ -57,7 +58,7 @@ public void Can_Attach_1_With_Multiple_Files() _file.Setup(f => f.Exists(It.IsAny())).Returns(true); _file.Setup(f => f.TempFile(It.IsAny())).Returns("attach.zip"); - var attacher = new Attacher(new ExceptionReportInfo { FilesToAttach = new[] { "file1", "file2", "file3" } }) + var attacher = new Attacher(new ExceptionReportInfo { FilesToAttach = new[] { "file1", "file2", "file3" } }, new Mock().Object) { FileService = _file.Object, Zipper = _zip.Object @@ -74,7 +75,7 @@ public void Can_Attach_0_When_Multiple_Files_But_None_Exist() _file.Setup(f => f.Exists(It.IsAny())).Returns(false); _file.Setup(f => f.TempFile(It.IsAny())).Returns("attach.zip"); - var attacher = new Attacher(new ExceptionReportInfo { FilesToAttach = new[] { "file1", "file2", "file3" } }) + var attacher = new Attacher(new ExceptionReportInfo { FilesToAttach = new[] { "file1", "file2", "file3" } }, new Mock().Object) { FileService = _file.Object, Zipper = _zip.Object @@ -89,7 +90,7 @@ public void Can_Attach_0_When_Multiple_Files_But_None_Exist() public void Can_Attach_1_For_Each_Already_Zipped() { _file.Setup(f => f.Exists(It.IsAny())).Returns(true); - var attacher = new Attacher(new ExceptionReportInfo { FilesToAttach = new[] { "file1.zip", "file2.zip" } }) + var attacher = new Attacher(new ExceptionReportInfo { FilesToAttach = new[] { "file1.zip", "file2.zip" } }, new Mock().Object) { FileService = _file.Object, Zipper = _zip.Object @@ -107,7 +108,7 @@ public void Can_Attach_1_Already_Zipped_And_1_File_To_Attach() _file.Setup(f => f.TempFile(It.IsAny())).Returns("attach.zip"); _file.Setup(f => f.Exists(It.IsAny())).Returns(true); - var attacher = new Attacher(new ExceptionReportInfo { FilesToAttach = new[] { "file1.zip" , "file2.txt"} }) + var attacher = new Attacher(new ExceptionReportInfo { FilesToAttach = new[] { "file1.zip" , "file2.txt"} }, new Mock().Object) { FileService = _file.Object, Zipper = _zip.Object @@ -122,11 +123,11 @@ public void Can_Attach_1_Already_Zipped_And_1_File_To_Attach() [Test] public void Should_Take_Screenshot_If_Configured() { - var screenshot = new Mock(); + var screenshot = new Mock(); var attacher = - new Attacher(new ExceptionReportInfo {TakeScreenshot = true}) + new Attacher(new ExceptionReportInfo {TakeScreenshot = true}, new Mock().Object) { - ScreenshotTaker = screenshot.Object + ScreenShooter = screenshot.Object }; attacher.AttachFiles(_iAttach.Object); @@ -137,11 +138,11 @@ public void Should_Take_Screenshot_If_Configured() [Test] public void Should_Not_Take_Screenshot_If_Not_Configured() { - var screenshot = new Mock(); + var screenshot = new Mock(); var attacher = - new Attacher(new ExceptionReportInfo { TakeScreenshot = false }) + new Attacher(new ExceptionReportInfo { TakeScreenshot = false }, new Mock().Object) { - ScreenshotTaker = screenshot.Object + ScreenShooter = screenshot.Object }; attacher.AttachFiles(_iAttach.Object); diff --git a/src/Tests/ExceptionReporter_Tests.cs b/src/Tests/ExceptionReporter_Tests.cs index 6d6b66ae..9c6955d1 100644 --- a/src/Tests/ExceptionReporter_Tests.cs +++ b/src/Tests/ExceptionReporter_Tests.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using ExceptionReporting; -using ExceptionReporting.MVP.Views; +using ExceptionReporting.Core; using Moq; using NUnit.Framework; diff --git a/src/Tests/MailSender_Tests.cs b/src/Tests/MailSender_Tests.cs index 42f4a3e9..db3934e1 100644 --- a/src/Tests/MailSender_Tests.cs +++ b/src/Tests/MailSender_Tests.cs @@ -1,6 +1,9 @@ using System; using ExceptionReporting; +using ExceptionReporting.Core; using ExceptionReporting.Network.Senders; +using ExceptionReporting.Report; +using Moq; using NUnit.Framework; namespace Tests.ExceptionReporting @@ -13,7 +16,7 @@ public void Can_Create_Subject() var exception = new Exception("hello"); var reportInfo = new ExceptionReportInfo { TitleText = "test" }; reportInfo.SetExceptions(new[] { exception }); - var mailSender = new MapiMailSender(reportInfo, null); + var mailSender = new MapiMailSender(reportInfo, null, new Mock().Object); Assert.That(mailSender.EmailSubject, Is.EqualTo("hello")); } @@ -26,7 +29,7 @@ public void Can_Create_Subject_Without_CrLf() { var reportInfo = new ExceptionReportInfo(); reportInfo.SetExceptions(new[] { new Exception("hello\r\nagain") }); - var mailSender = new MapiMailSender(reportInfo, null); + var mailSender = new MapiMailSender(reportInfo, null, new Mock().Object); Assert.That(mailSender.EmailSubject, Does.Not.Contain("\r")); Assert.That(mailSender.EmailSubject, Does.Not.Contain("\n")); @@ -35,7 +38,7 @@ public void Can_Create_Subject_Without_CrLf() [Test] public void Can_Create_Subject_If_Exception_Is_Null() { - var mailSender = new MapiMailSender(new ExceptionReportInfo(), null); // no exceptions set, so message will be null, does mail cater for it? + var mailSender = new MapiMailSender(new ExceptionReportInfo(), null, new Mock().Object); // no exceptions set, so message will be null, does mail cater for it? Assert.That(mailSender.EmailSubject, Is.EqualTo("Empty Exception")); // reverts to a default message } @@ -47,7 +50,7 @@ public void Can_Use_Custom_Subject() var reportInfo = new ExceptionReportInfo { TitleText = "test" }; reportInfo.SetExceptions(new[] { exception }); reportInfo.EmailReportSubject = "hello"; - var mailSender = new MapiMailSender(reportInfo, null); + var mailSender = new MapiMailSender(reportInfo, null, new Mock().Object); Assert.That(mailSender.EmailSubject, Is.EqualTo("hello")); } diff --git a/src/Tests/ReportGenerator_Tests.cs b/src/Tests/ReportGenerator_Tests.cs index 8f1b7f62..f52aef31 100644 --- a/src/Tests/ReportGenerator_Tests.cs +++ b/src/Tests/ReportGenerator_Tests.cs @@ -29,7 +29,6 @@ public void Can_Deal_With_Null_In_Constructor() [Test] public void Can_Create_Report_With_A_Couple_Of_Minimal_Bits_That_Should_Exist() { - if (ExceptionReporter.IsRunningMono()) return; var report = _reportGenerator.Generate(); var reportString = report; diff --git a/src/Tests/SenderFactory_Tests.cs b/src/Tests/SenderFactory_Tests.cs index 70354346..81e09540 100644 --- a/src/Tests/SenderFactory_Tests.cs +++ b/src/Tests/SenderFactory_Tests.cs @@ -1,8 +1,10 @@ -using System; +using System; using ExceptionReporting; +using ExceptionReporting.Core; using ExceptionReporting.Network; using ExceptionReporting.Network.Events; using ExceptionReporting.Network.Senders; +using ExceptionReporting.Report; using Moq; using NUnit.Framework; @@ -19,7 +21,7 @@ public Type Can_Determine_Sender(ReportSendMethod method) var factory = new SenderFactory(new ExceptionReportInfo { SendMethod = method - }, new Mock().Object); + }, new Mock().Object, new Mock().Object); return factory.Get().GetType(); } diff --git a/src/Tests/SystemInfoRetriever_Tests.cs b/src/Tests/SystemInfoRetriever_Tests.cs index 6bef7027..954a68b9 100644 --- a/src/Tests/SystemInfoRetriever_Tests.cs +++ b/src/Tests/SystemInfoRetriever_Tests.cs @@ -18,11 +18,6 @@ public class SystemInfoRetriever_Tests public void Can_Retrieve_SysInfo_For_CPU() { var sysInfoResult = _retriever.Retrieve(SysInfoQueries.Machine); - if (ExceptionReporter.IsRunningMono()) - { - Assert.That(sysInfoResult, Is.Null); - return; - }; Assert.That(sysInfoResult.Nodes.Count, Is.EqualTo(1)); // at least 1 machine name Assert.That(sysInfoResult.ChildResults[0].Nodes.Count, Is.GreaterThan(0)); @@ -33,11 +28,6 @@ public void Can_Retrieve_SysInfo_For_CPU() public void Can_Retrieve_SysInfo_For_OS() { var sysInfoResult = _retriever.Retrieve(SysInfoQueries.OperatingSystem); - if (ExceptionReporter.IsRunningMono()) - { - Assert.That(sysInfoResult, Is.Null); - return; - }; Assert.That(sysInfoResult.Nodes[0], Does.Contain("Windows")); } } diff --git a/src/Tests/Tests.ExceptionReporter.NET.csproj b/src/Tests/Tests.ExceptionReporter.NET.csproj index ccbdc60e..09edb584 100644 --- a/src/Tests/Tests.ExceptionReporter.NET.csproj +++ b/src/Tests/Tests.ExceptionReporter.NET.csproj @@ -1,6 +1,6 @@  - net452 + net472 Tests.ExceptionReporting 4.0.3 @@ -15,6 +15,15 @@ - + + + + + + + + + True + \ No newline at end of file diff --git a/src/Tests/ZipReportService_Tests.cs b/src/Tests/ZipReportService_Tests.cs index 3c396bec..b4feaca1 100644 --- a/src/Tests/ZipReportService_Tests.cs +++ b/src/Tests/ZipReportService_Tests.cs @@ -2,7 +2,7 @@ using System.Linq; using ExceptionReporting; using ExceptionReporting.Core; -using ExceptionReporting.Mail; +using ExceptionReporting.Zip; using Moq; using NUnit.Framework; @@ -10,14 +10,14 @@ namespace Tests.ExceptionReporting { public class ZipReportService_Tests { - private Mock _screenshotTaker; + private Mock _screenshotTaker; private Mock _fileService; private Mock _zipper; [SetUp] public void SetUp() { - _screenshotTaker = new Mock(); + _screenshotTaker = new Mock(); _fileService = new Mock(); _zipper = new Mock(); } @@ -36,7 +36,7 @@ public void None_Files_To_Add_To_Archive_ReturnsEmptyString() TakeScreenshot = false, AttachmentFilename = zipFilename }; - var zip = new ZipReportService(_zipper.Object, _screenshotTaker.Object, _fileService.Object); + var zip = new ZipAttachmentService(_zipper.Object, _screenshotTaker.Object, _fileService.Object); var result = zip.CreateZipReport(config); Assert.IsTrue(result == string.Empty); @@ -67,7 +67,7 @@ public void Two_Files_But_None_Exists_ReturnsEmptyString() TakeScreenshot = false, AttachmentFilename = zipFilename }; - var zip = new ZipReportService(_zipper.Object, _screenshotTaker.Object, _fileService.Object); + var zip = new ZipAttachmentService(_zipper.Object, _screenshotTaker.Object, _fileService.Object); var result = zip.CreateZipReport(config); Assert.That(result, Is.EqualTo(string.Empty)); @@ -91,7 +91,7 @@ public void One_File_To_Add_To_Archive_ReturnsArchiveWithOneFile() TakeScreenshot = false, AttachmentFilename = zipFilename }; - var zip = new ZipReportService(_zipper.Object, _screenshotTaker.Object, _fileService.Object); + var zip = new ZipAttachmentService(_zipper.Object, _screenshotTaker.Object, _fileService.Object); var result = zip.CreateZipReport(config); Assert.That(result, Is.EqualTo(zipFilename)); @@ -116,7 +116,7 @@ public void Only_Screenshot_To_Add_To_Archive_ReturnsArchiveWithOneFile() TakeScreenshot = true, AttachmentFilename = zipFilename }; - var zip = new ZipReportService(_zipper.Object, _screenshotTaker.Object, _fileService.Object); + var zip = new ZipAttachmentService(_zipper.Object, _screenshotTaker.Object, _fileService.Object); var result = zip.CreateZipReport(config); Assert.That(result, Is.EqualTo(zipFilename)); @@ -140,7 +140,7 @@ public void Two_Files_To_Add_To_Archive_ReturnsArchiveWithTwoFiles() { FilesToAttach = filesToAttach.ToArray(), TakeScreenshot = false, AttachmentFilename = zipFilename }; - var zip = new ZipReportService(_zipper.Object, _screenshotTaker.Object, _fileService.Object); + var zip = new ZipAttachmentService(_zipper.Object, _screenshotTaker.Object, _fileService.Object); var result = zip.CreateZipReport(config); Assert.That(result, Is.EqualTo(zipFilename)); @@ -162,7 +162,7 @@ public void Take_Screenshot_True_MakesScreenshot() TakeScreenshot = true, AttachmentFilename = zipFilename }; - var zip = new ZipReportService(_zipper.Object, _screenshotTaker.Object, _fileService.Object); + var zip = new ZipAttachmentService(_zipper.Object, _screenshotTaker.Object, _fileService.Object); zip.CreateZipReport(config); _screenshotTaker.Verify(s => s.TakeScreenShot(), Times.Once()); @@ -183,7 +183,7 @@ public void Take_Screenshot_False_DoesNotMakeScreenshot() TakeScreenshot = false, AttachmentFilename = zipFilename }; - var zip = new ZipReportService(_zipper.Object, _screenshotTaker.Object, _fileService.Object); + var zip = new ZipAttachmentService(_zipper.Object, _screenshotTaker.Object, _fileService.Object); zip.CreateZipReport(config); _screenshotTaker.Verify(s => s.TakeScreenShot(), Times.Never());