├── Simple.Wpf.Exceptions ├── Services │ ├── IService.cs │ ├── IGestureService.cs │ ├── IApplicationService.cs │ ├── IOverlayService.cs │ ├── IMessageService.cs │ ├── ISchedulerService.cs │ ├── OverlayService.cs │ ├── SchedulerService.cs │ ├── GesturesService.cs │ ├── ApplicationService.cs │ └── MessageService.cs ├── ViewModels │ ├── IMainViewModel.cs │ ├── ITransientViewModel.cs │ ├── IExceptionViewModel.cs │ ├── IViewModel.cs │ ├── ICloseableViewModel.cs │ ├── IChromeViewModel.cs │ ├── OverlayViewModel.cs │ ├── ChromeViewModel.cs │ ├── CloseableViewModel.cs │ ├── MainViewModel.cs │ ├── ExceptionViewModel.cs │ └── BaseViewModel.cs ├── Simple.Wpf.Exceptions.v2.ncrunchproject ├── Properties │ ├── Settings.settings │ ├── AssemblyInfo.cs │ ├── Settings.Designer.cs │ ├── Resources.Designer.cs │ └── Resources.resx ├── Views │ ├── Converters.xaml │ ├── Styles.xaml │ ├── MessageDialog.cs │ ├── MainWindow.xaml │ ├── MainWindow.xaml.cs │ └── Templates.xaml ├── Extensions │ ├── UnitExtensions.cs │ ├── AddRangeExtensions.cs │ ├── ToObservableCollectionExtensions.cs │ ├── CompositeDisposableExtensions.cs │ ├── ForEachExtensions.cs │ ├── NotifyCollectionChangedExtensions.cs │ ├── SchedulerExtensions.cs │ ├── NotifyPropertyChangedExtensions.cs │ └── ObservableExtensions.cs ├── Models │ ├── Message.cs │ └── DisposableObject.cs ├── Helpers │ ├── ExpressionHelper.cs │ └── LogHelper.cs ├── Constants.cs ├── NLogFormattedThreadIdLayoutRenderer.cs ├── App.xaml ├── Duration.cs ├── NLog.config ├── Collections │ └── RangeObservableCollection.cs ├── packages.config ├── Commands │ └── ReactiveCommand.cs ├── app.config ├── BootStrapper.cs ├── App.xaml.cs └── Simple.Wpf.Exceptions.csproj ├── Simple.Wpf.Exceptions.Tests ├── TestSchedulerExtensions.cs ├── NLog.config ├── TestHelper.cs ├── MockSchedulerService.cs ├── packages.config ├── Properties │ └── AssemblyInfo.cs ├── app.config ├── MessageServiceFixtures.cs ├── MainViewModelFixtures.cs ├── ExceptionViewModelFixtures.cs └── Simple.Wpf.Exceptions.Tests.csproj ├── Simple.Wpf.Exceptions.sln ├── README.md └── .gitignore /Simple.Wpf.Exceptions/Services/IService.cs: -------------------------------------------------------------------------------- 1 | namespace Simple.Wpf.Exceptions.Services 2 | { 3 | public interface IService 4 | { 5 | } 6 | } -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/ViewModels/IMainViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace Simple.Wpf.Exceptions.ViewModels 2 | { 3 | public interface IMainViewModel 4 | { 5 | } 6 | } -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/Simple.Wpf.Exceptions.v2.ncrunchproject: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oriches/Simple.Wpf.Exceptions/HEAD/Simple.Wpf.Exceptions/Simple.Wpf.Exceptions.v2.ncrunchproject -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/ViewModels/ITransientViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace Simple.Wpf.Exceptions.ViewModels 2 | { 3 | public interface ITransientViewModel : IViewModel 4 | { 5 | } 6 | } -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/ViewModels/IExceptionViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace Simple.Wpf.Exceptions.ViewModels 2 | { 3 | public interface IExceptionViewModel : ICloseableViewModel 4 | { 5 | } 6 | } -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/Services/IGestureService.cs: -------------------------------------------------------------------------------- 1 | namespace Simple.Wpf.Exceptions.Services 2 | { 3 | public interface IGestureService : IService 4 | { 5 | void SetBusy(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/Properties/Settings.settings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/ViewModels/IViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace Simple.Wpf.Exceptions.ViewModels 2 | { 3 | using System; 4 | using System.ComponentModel; 5 | 6 | public interface IViewModel : IDisposable, INotifyPropertyChanged 7 | { 8 | IDisposable SuspendNotifications(); 9 | } 10 | } -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/Views/Converters.xaml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/Services/IApplicationService.cs: -------------------------------------------------------------------------------- 1 | namespace Simple.Wpf.Exceptions.Services 2 | { 3 | public interface IApplicationService : IService 4 | { 5 | string LogFolder { get; } 6 | 7 | void CopyToClipboard(string text); 8 | void Exit(); 9 | void Restart(); 10 | void OpenFolder(string folder); 11 | } 12 | } -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/Services/IOverlayService.cs: -------------------------------------------------------------------------------- 1 | namespace Simple.Wpf.Exceptions.Services 2 | { 3 | using System; 4 | using ViewModels; 5 | 6 | public interface IOverlayService : IService 7 | { 8 | IObservable Show { get; } 9 | 10 | void Post(string header, BaseViewModel viewModel, IDisposable lifetime); 11 | } 12 | } -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/Services/IMessageService.cs: -------------------------------------------------------------------------------- 1 | namespace Simple.Wpf.Exceptions.Services 2 | { 3 | using System; 4 | using Models; 5 | using ViewModels; 6 | 7 | public interface IMessageService : IService 8 | { 9 | IObservable Show { get; } 10 | 11 | void Post(string header, ICloseableViewModel viewModel); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/ViewModels/ICloseableViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace Simple.Wpf.Exceptions.ViewModels 2 | { 3 | using System; 4 | using System.Reactive; 5 | 6 | public interface ICloseableViewModel : ITransientViewModel 7 | { 8 | IObservable Closed { get; } 9 | IObservable Denied { get; } 10 | IObservable Confirmed { get; } 11 | } 12 | } -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions.Tests/TestSchedulerExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Reactive.Testing; 3 | 4 | namespace Simple.Wpf.Exceptions.Tests 5 | { 6 | public static class TestSchedulerExtensions 7 | { 8 | public static void AdvanceBy(this TestScheduler testScheduler, TimeSpan timeSpan) 9 | { 10 | testScheduler.AdvanceBy(timeSpan.Ticks); 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/ViewModels/IChromeViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace Simple.Wpf.Exceptions.ViewModels 2 | { 3 | using Commands; 4 | 5 | public interface IChromeViewModel : IViewModel 6 | { 7 | IMainViewModel Main { get; } 8 | ReactiveCommand CloseOverlayCommand { get; } 9 | bool HasOverlay { get; } 10 | string OverlayHeader { get; } 11 | BaseViewModel Overlay { get; } 12 | } 13 | } -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/Extensions/UnitExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Simple.Wpf.Exceptions.Extensions 2 | { 3 | using System; 4 | using System.Reactive; 5 | using System.Reactive.Linq; 6 | 7 | public static class UnitExtensions 8 | { 9 | public static IObservable AsUnit(this IObservable observable) 10 | { 11 | return observable.Select(x => Unit.Default); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/Extensions/AddRangeExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Simple.Wpf.Exceptions.Extensions 4 | { 5 | public static class AddRangeExtensions 6 | { 7 | public static void AddRange(this ICollection collection, IEnumerable enumerable) 8 | { 9 | foreach (var item in enumerable) 10 | { 11 | collection.Add(item); 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/Models/Message.cs: -------------------------------------------------------------------------------- 1 | namespace Simple.Wpf.Exceptions.Models 2 | { 3 | using ViewModels; 4 | 5 | public sealed class Message 6 | { 7 | public Message(string header, ICloseableViewModel viewModel) 8 | { 9 | Header = header; 10 | ViewModel = viewModel; 11 | } 12 | 13 | public string Header { get; private set; } 14 | 15 | public ICloseableViewModel ViewModel { get; private set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/Extensions/ToObservableCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Simple.Wpf.Exceptions.Extensions 2 | { 3 | using System.Collections.Generic; 4 | using System.Collections.ObjectModel; 5 | 6 | public static class ToObservableCollectionExtensions 7 | { 8 | public static ObservableCollection ToObservableCollection(this IEnumerable enumerable) 9 | { 10 | return new ObservableCollection(enumerable); 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/Extensions/CompositeDisposableExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Simple.Wpf.Exceptions.Extensions 2 | { 3 | using System; 4 | using System.Reactive.Disposables; 5 | 6 | public static class CompositeDisposableExtensions 7 | { 8 | public static T DisposeWith(this T instance, CompositeDisposable disposable) where T : IDisposable 9 | { 10 | disposable.Add(instance); 11 | 12 | return instance; 13 | 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/Services/ISchedulerService.cs: -------------------------------------------------------------------------------- 1 | namespace Simple.Wpf.Exceptions.Services 2 | { 3 | using System.Reactive.Concurrency; 4 | 5 | public interface ISchedulerService : IService 6 | { 7 | IScheduler Dispatcher { get; } 8 | 9 | IScheduler Current { get; } 10 | 11 | IScheduler TaskPool { get; } 12 | 13 | IScheduler EventLoop { get; } 14 | 15 | IScheduler NewThread { get; } 16 | 17 | IScheduler StaThread { get; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/Helpers/ExpressionHelper.cs: -------------------------------------------------------------------------------- 1 | namespace Simple.Wpf.Exceptions.Helpers 2 | { 3 | using System; 4 | using System.Linq.Expressions; 5 | 6 | public static class ExpressionHelper 7 | { 8 | public static string Name(Expression> expression) 9 | { 10 | var lambda = expression as LambdaExpression; 11 | var memberExpression = (MemberExpression)lambda.Body; 12 | 13 | return memberExpression.Member.Name; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/Extensions/ForEachExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Simple.Wpf.Exceptions.Extensions 5 | { 6 | public static class ForEachExtensions 7 | { 8 | public static IEnumerable ForEach(this IEnumerable enumerable, Action action) 9 | { 10 | foreach (var i in enumerable) 11 | { 12 | action(i); 13 | } 14 | 15 | return enumerable; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions.Tests/NLog.config: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/Views/Styles.xaml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 12 | 13 | -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions.Tests/TestHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Reflection; 4 | 5 | namespace Simple.Wpf.Exceptions.Tests 6 | { 7 | public static class TestHelper 8 | { 9 | public static IEnumerable PropertiesImplementingInterface(object instance) 10 | { 11 | return instance.GetType() 12 | .GetProperties() 13 | .Where(x => x.PropertyType == typeof(T) || x.PropertyType.GetInterfaces().Any(y => y == typeof(T))); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/Views/MessageDialog.cs: -------------------------------------------------------------------------------- 1 | namespace Simple.Wpf.Exceptions.Views 2 | { 3 | using System.Windows.Markup; 4 | using MahApps.Metro.Controls.Dialogs; 5 | using Models; 6 | using ViewModels; 7 | 8 | [ContentProperty("DialogBody")] 9 | public sealed class MessageDialog : BaseMetroDialog 10 | { 11 | private readonly Message _message; 12 | 13 | public MessageDialog(Message message) 14 | { 15 | _message = message; 16 | 17 | Title = _message.Header; 18 | Content = _message.ViewModel; 19 | } 20 | 21 | public ICloseableViewModel CloseableContent => _message.ViewModel; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/Extensions/NotifyCollectionChangedExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Simple.Wpf.Exceptions.Extensions 2 | { 3 | using System; 4 | using System.Collections.Specialized; 5 | using System.Reactive.Linq; 6 | 7 | public static class NotifyCollectionChangedExtensions 8 | { 9 | public static IObservable ObserveCollectionChanged(this INotifyCollectionChanged source) 10 | { 11 | return Observable.FromEventPattern( 12 | h => source.CollectionChanged += h, h => source.CollectionChanged -= h) 13 | .Select(x => x.EventArgs); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | using System.Windows; 4 | 5 | [assembly: AssemblyTitle("Simple.Wpf.Exceptions")] 6 | [assembly: AssemblyDescription("")] 7 | [assembly: AssemblyConfiguration("")] 8 | [assembly: AssemblyCompany("")] 9 | [assembly: AssemblyProduct("Simple.Wpf.Exceptions")] 10 | [assembly: AssemblyCopyright("")] 11 | [assembly: AssemblyTrademark("")] 12 | [assembly: AssemblyCulture("")] 13 | 14 | [assembly: ComVisible(false)] 15 | 16 | [assembly: ThemeInfo( 17 | ResourceDictionaryLocation.None, 18 | ResourceDictionaryLocation.SourceAssembly 19 | )] 20 | 21 | 22 | [assembly: AssemblyVersion("1.0.0.0")] 23 | [assembly: AssemblyFileVersion("1.0.0.0")] 24 | -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/Extensions/SchedulerExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Simple.Wpf.Exceptions.Extensions 2 | { 3 | using System; 4 | using System.Reactive.Concurrency; 5 | using System.Reactive.Disposables; 6 | 7 | public static class SchedulerExtensions 8 | { 9 | public static IDisposable Schedule(this IScheduler scheduler, TimeSpan timeSpan, Action action) 10 | { 11 | return scheduler.Schedule(null, timeSpan, (s1, s2) => 12 | { 13 | action(); 14 | return Disposable.Empty; 15 | }); 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions.Tests/MockSchedulerService.cs: -------------------------------------------------------------------------------- 1 | using System.Reactive.Concurrency; 2 | using Microsoft.Reactive.Testing; 3 | using Simple.Wpf.Exceptions.Services; 4 | 5 | namespace Simple.Wpf.Exceptions.Tests 6 | { 7 | public sealed class MockSchedulerService : ISchedulerService 8 | { 9 | private readonly TestScheduler _testScheduler; 10 | 11 | public MockSchedulerService(TestScheduler testScheduler) 12 | { 13 | _testScheduler = testScheduler; 14 | } 15 | 16 | public IScheduler Dispatcher => _testScheduler; 17 | 18 | public IScheduler Current => _testScheduler; 19 | 20 | public IScheduler TaskPool => _testScheduler; 21 | 22 | public IScheduler EventLoop => _testScheduler; 23 | 24 | public IScheduler NewThread => _testScheduler; 25 | 26 | public IScheduler StaThread => _testScheduler; 27 | } 28 | } -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/Models/DisposableObject.cs: -------------------------------------------------------------------------------- 1 | namespace Simple.Wpf.Exceptions.Models 2 | { 3 | using System; 4 | using System.Reactive.Disposables; 5 | using NLog; 6 | 7 | public abstract class DisposableObject : IDisposable 8 | { 9 | protected static readonly Logger Logger = LogManager.GetCurrentClassLogger(); 10 | 11 | private readonly CompositeDisposable _disposable; 12 | 13 | protected DisposableObject() 14 | { 15 | _disposable = new CompositeDisposable(); 16 | } 17 | 18 | public virtual void Dispose() 19 | { 20 | using (Duration.Measure(Logger, "Dispose - " + GetType().Name)) 21 | _disposable.Dispose(); 22 | } 23 | 24 | public static implicit operator CompositeDisposable(DisposableObject disposable) 25 | { 26 | return disposable._disposable; 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/ViewModels/OverlayViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace Simple.Wpf.Exceptions.ViewModels 2 | { 3 | using System; 4 | 5 | public abstract class OverlayViewModel 6 | { 7 | protected OverlayViewModel(string header, T viewModel, IDisposable lifetime) 8 | { 9 | Header = header; 10 | ViewModel = viewModel; 11 | Lifetime = lifetime; 12 | } 13 | 14 | public string Header { get; private set; } 15 | 16 | public T ViewModel { get; private set; } 17 | 18 | public IDisposable Lifetime { get; } 19 | 20 | public bool HasLifetime => Lifetime != null; 21 | } 22 | 23 | public sealed class OverlayViewModel : OverlayViewModel 24 | { 25 | public OverlayViewModel(string header, BaseViewModel viewModel, IDisposable lifetime) 26 | : base(header, viewModel, lifetime) 27 | { 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/Services/OverlayService.cs: -------------------------------------------------------------------------------- 1 | namespace Simple.Wpf.Exceptions.Services 2 | { 3 | using System; 4 | using System.Reactive.Subjects; 5 | using Exceptions; 6 | using Extensions; 7 | using Models; 8 | using ViewModels; 9 | 10 | public sealed class OverlayService : DisposableObject, IOverlayService 11 | { 12 | private readonly Subject _show; 13 | 14 | public OverlayService() 15 | { 16 | using (Duration.Measure(Logger, "Constructor - " + GetType().Name)) 17 | { 18 | _show = new Subject() 19 | .DisposeWith(this); 20 | } 21 | } 22 | 23 | public void Post(string header, BaseViewModel viewModel, IDisposable lifetime) 24 | { 25 | _show.OnNext(new OverlayViewModel(header, viewModel, lifetime)); 26 | } 27 | 28 | public IObservable Show => _show; 29 | } 30 | } -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace Simple.Wpf.Exceptions 2 | { 3 | using System; 4 | 5 | public static class Constants 6 | { 7 | public static readonly TimeSpan Heartbeat = TimeSpan.FromSeconds(5); 8 | public static readonly TimeSpan UiFreeze = TimeSpan.FromMilliseconds(500); 9 | public static readonly TimeSpan UiFreezeTimer = TimeSpan.FromMilliseconds(333); 10 | 11 | public static readonly TimeSpan DiagnosticsLogInterval = TimeSpan.FromSeconds(1); 12 | public static readonly TimeSpan DiagnosticsIdleBuffer = TimeSpan.FromMilliseconds(666); 13 | public static readonly TimeSpan DiagnosticsCpuBuffer = TimeSpan.FromMilliseconds(666); 14 | public static readonly TimeSpan DiagnosticsSubscriptionDelay = TimeSpan.FromMilliseconds(1000); 15 | 16 | public const string DefaultRpsString = "Render: 00 RPS"; 17 | public const string DefaultCpuString = "CPU: 00 %"; 18 | public const string DefaultManagedMemoryString = "Managed Memory: 00 Mb"; 19 | public const string DefaultTotalMemoryString = "Total Memory: 00 Mb"; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/NLogFormattedThreadIdLayoutRenderer.cs: -------------------------------------------------------------------------------- 1 | namespace Simple.Wpf.Exceptions 2 | { 3 | using System; 4 | using System.Globalization; 5 | using System.Text; 6 | using NLog; 7 | using NLog.LayoutRenderers; 8 | 9 | [LayoutRenderer("formatted_threadid")] 10 | public sealed class NLogFormattedThreadIdLayoutRenderer : LayoutRenderer 11 | { 12 | private readonly Func _threadIdFunc; 13 | 14 | public NLogFormattedThreadIdLayoutRenderer() 15 | { 16 | _threadIdFunc = () => System.Threading.Thread.CurrentThread.ManagedThreadId; 17 | } 18 | 19 | public NLogFormattedThreadIdLayoutRenderer(Func threadIdFunc) 20 | { 21 | _threadIdFunc = threadIdFunc; 22 | } 23 | 24 | protected override void Append(StringBuilder builder, LogEventInfo logEvent) 25 | { 26 | var threadId = _threadIdFunc(); 27 | if (threadId < 10) 28 | { 29 | builder.Append("0"); 30 | } 31 | 32 | builder.Append(threadId.ToString(CultureInfo.InvariantCulture)); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/Properties/Settings.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace Simple.Wpf.Exceptions.Properties { 12 | 13 | 14 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 15 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.3.0.0")] 16 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { 17 | 18 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); 19 | 20 | public static Settings Default { 21 | get { 22 | return defaultInstance; 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/App.xaml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions.Tests/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/Helpers/LogHelper.cs: -------------------------------------------------------------------------------- 1 | namespace Simple.Wpf.Exceptions.Helpers 2 | { 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using Extensions; 6 | using NLog; 7 | 8 | public static class LogHelper 9 | { 10 | private static readonly IEnumerable AllLevels = new[] 11 | { 12 | LogLevel.Trace, 13 | LogLevel.Debug, 14 | LogLevel.Info, 15 | LogLevel.Warn, 16 | LogLevel.Error, 17 | LogLevel.Fatal, 18 | }; 19 | 20 | public static void ReconfigureLoggerToLevel(LogLevel level) 21 | { 22 | var disableLevels = AllLevels.Where(x => x < level) 23 | .ToArray(); 24 | 25 | var enableLevels = AllLevels.Where(x => x >= level) 26 | .ToArray(); 27 | 28 | foreach (var rule in LogManager.Configuration.LoggingRules) 29 | { 30 | var localRule = rule; 31 | 32 | disableLevels.ForEach(localRule.DisableLoggingForLevel); 33 | enableLevels.ForEach(localRule.EnableLoggingForLevel); 34 | } 35 | 36 | LogManager.ReconfigExistingLoggers(); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/Extensions/NotifyPropertyChangedExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Simple.Wpf.Exceptions.Extensions 2 | { 3 | using System; 4 | using System.ComponentModel; 5 | using System.Linq; 6 | using System.Linq.Expressions; 7 | using System.Reactive.Linq; 8 | 9 | public static class NotifyPropertyChangedExtensions 10 | { 11 | public static IObservable ObservePropertyChanged(this TSource source, 12 | params Expression>[] properties) 13 | where TSource : INotifyPropertyChanged 14 | { 15 | var names = properties.Select(x => x.Body) 16 | .OfType() 17 | .Select(x => x.Member.Name); 18 | 19 | return source.ObservePropertyChanged() 20 | .Where(x => names.Contains(x.PropertyName)); 21 | } 22 | 23 | public static IObservable ObservePropertyChanged(this INotifyPropertyChanged source) 24 | { 25 | return Observable.FromEventPattern( 26 | h => source.PropertyChanged += h, h => source.PropertyChanged -= h) 27 | .Select(x => x.EventArgs); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/Services/SchedulerService.cs: -------------------------------------------------------------------------------- 1 | namespace Simple.Wpf.Exceptions.Services 2 | { 3 | using System; 4 | using System.Reactive.Concurrency; 5 | using System.Threading; 6 | 7 | public sealed class SchedulerService : ISchedulerService 8 | { 9 | private readonly DispatcherScheduler _dispatcherScheduler; 10 | 11 | public SchedulerService() 12 | { 13 | _dispatcherScheduler = DispatcherScheduler.Current; 14 | } 15 | 16 | public IScheduler Dispatcher => _dispatcherScheduler; 17 | 18 | public IScheduler Current => CurrentThreadScheduler.Instance; 19 | 20 | public IScheduler TaskPool => TaskPoolScheduler.Default; 21 | 22 | public IScheduler EventLoop => new EventLoopScheduler(); 23 | 24 | public IScheduler NewThread => NewThreadScheduler.Default; 25 | 26 | public IScheduler StaThread 27 | { 28 | get 29 | { 30 | Func func = x => 31 | { 32 | var thread = new Thread(x) { IsBackground = true }; 33 | thread.SetApartmentState(ApartmentState.STA); 34 | 35 | return thread; 36 | }; 37 | 38 | return new EventLoopScheduler(func); 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/Duration.cs: -------------------------------------------------------------------------------- 1 | namespace Simple.Wpf.Exceptions 2 | { 3 | using System; 4 | using System.Diagnostics; 5 | using System.Globalization; 6 | using System.Reactive.Disposables; 7 | using NLog; 8 | 9 | public sealed class Duration : IDisposable 10 | { 11 | private readonly string _context; 12 | private readonly Stopwatch _stopWatch; 13 | private readonly Logger _logger; 14 | 15 | private Duration(Logger logger, string context) 16 | { 17 | _context = context; 18 | _stopWatch = new Stopwatch(); 19 | _logger = logger; 20 | 21 | _stopWatch.Start(); 22 | } 23 | 24 | public static IDisposable Measure(Logger logger, string context, params object[] args) 25 | { 26 | if (!logger.IsDebugEnabled) 27 | { 28 | return Disposable.Empty; 29 | } 30 | 31 | if (args != null) 32 | { 33 | context = string.Format(CultureInfo.InvariantCulture, context, args); 34 | } 35 | 36 | return new Duration(logger, context); 37 | } 38 | 39 | public void Dispose() 40 | { 41 | _stopWatch.Stop(); 42 | 43 | _logger.Debug(CultureInfo.InvariantCulture, "{0}, duration = {1} ms", _context, _stopWatch.ElapsedMilliseconds); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/Services/GesturesService.cs: -------------------------------------------------------------------------------- 1 | namespace Simple.Wpf.Exceptions.Services 2 | { 3 | using System; 4 | using System.Reactive.Disposables; 5 | using System.Windows; 6 | using System.Windows.Input; 7 | using System.Windows.Threading; 8 | using Extensions; 9 | using Models; 10 | 11 | public sealed class GesturesService : DisposableObject, IGestureService 12 | { 13 | private readonly DispatcherTimer _timer; 14 | private bool _isBusy; 15 | 16 | public GesturesService() 17 | { 18 | _timer = new DispatcherTimer(TimeSpan.Zero, DispatcherPriority.ApplicationIdle, TimerCallback, Application.Current.Dispatcher); 19 | _timer.Stop(); 20 | 21 | Disposable.Create(() => _timer.Stop()) 22 | .DisposeWith(this); 23 | } 24 | 25 | public void SetBusy() 26 | { 27 | SetBusyState(true); 28 | } 29 | 30 | private void SetBusyState(bool busy) 31 | { 32 | if (busy != _isBusy) 33 | { 34 | _isBusy = busy; 35 | Mouse.OverrideCursor = busy ? Cursors.Wait : null; 36 | 37 | if (_isBusy) 38 | { 39 | _timer.Start(); 40 | } 41 | } 42 | } 43 | 44 | private void TimerCallback(object sender, EventArgs e) 45 | { 46 | SetBusyState(false); 47 | _timer.Stop(); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions.Tests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | 8 | [assembly: AssemblyTitle("Simple.Wpf.Template.Tests")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("Simple.Wpf.Template.Tests")] 13 | [assembly: AssemblyCopyright("")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("85dcdc02-b5f2-46d6-bf81-cdd653718611")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/NLog.config: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 12 | 13 | 14 | 18 | 19 | 20 | 25 | 26 | 31 | 32 | 33 | 34 | 35 | 36 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/Views/MainWindow.xaml: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 14 4 | VisualStudioVersion = 14.0.25420.1 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Simple.Wpf.Exceptions", "Simple.Wpf.Exceptions\Simple.Wpf.Exceptions.csproj", "{3C3ED4EA-DC15-4286-B023-4951DFDDCB35}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Simple.Wpf.Exceptions.Tests", "Simple.Wpf.Exceptions.Tests\Simple.Wpf.Exceptions.Tests.csproj", "{7F37C3C0-31CF-45A4-AD11-0B2B661240D3}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {3C3ED4EA-DC15-4286-B023-4951DFDDCB35}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {3C3ED4EA-DC15-4286-B023-4951DFDDCB35}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {3C3ED4EA-DC15-4286-B023-4951DFDDCB35}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {3C3ED4EA-DC15-4286-B023-4951DFDDCB35}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {7F37C3C0-31CF-45A4-AD11-0B2B661240D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {7F37C3C0-31CF-45A4-AD11-0B2B661240D3}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {7F37C3C0-31CF-45A4-AD11-0B2B661240D3}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {7F37C3C0-31CF-45A4-AD11-0B2B661240D3}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | EndGlobal 29 | -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/Services/ApplicationService.cs: -------------------------------------------------------------------------------- 1 | namespace Simple.Wpf.Exceptions.Services 2 | { 3 | using System.Linq; 4 | using System.Windows; 5 | using NLog; 6 | using NLog.Targets; 7 | 8 | public sealed class ApplicationService : IApplicationService 9 | { 10 | private string _logFolder; 11 | 12 | public string LogFolder 13 | { 14 | get 15 | { 16 | if (!string.IsNullOrEmpty(_logFolder)) 17 | { 18 | return _logFolder; 19 | } 20 | 21 | _logFolder = GetLogFolder(); 22 | return _logFolder; 23 | } 24 | } 25 | 26 | public void CopyToClipboard(string text) 27 | { 28 | Clipboard.SetText(text); 29 | } 30 | 31 | public void Exit() 32 | { 33 | Application.Current.Shutdown(); 34 | } 35 | 36 | public void Restart() 37 | { 38 | System.Diagnostics.Process.Start(Application.ResourceAssembly.Location); 39 | Application.Current.Shutdown(); 40 | } 41 | 42 | public void OpenFolder(string folder) 43 | { 44 | System.Diagnostics.Process.Start("explorer.exe", folder); 45 | } 46 | 47 | private static string GetLogFolder() 48 | { 49 | var logFile = LogManager.Configuration.AllTargets 50 | .OfType() 51 | .Select(x => x.FileName as NLog.Layouts.SimpleLayout) 52 | .Select(x => x.Text) 53 | .FirstOrDefault(); 54 | 55 | return System.IO.Path.GetDirectoryName(logFile); 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple.Wpf.Exceptions 2 | 3 | [![Build status](https://ci.appveyor.com/api/projects/status/72staf3ivfet3699?svg=true)](https://ci.appveyor.com/project/oriches/simple-wpf-exceptions) 4 | 5 | As with all my 'important' stuff it builds using the amazing [AppVeyor](https://ci.appveyor.com/project/oriches/simple-wpf-exceptions). 6 | 7 | The app is skinned using [Mah Apps](http://mahapps.com/). 8 | 9 | This isn't a definitive guide or 'proper' way to do exception handling in a WPF application (if there is one), it's my way of dealing with unhandled exceptions, it is *my opinion* :) 10 | 11 | To many times I come across WPF applications which have either no or incomplete exception handling - typically observed as an application that just terminates unexpectedly without giving the user any info about why it crashed, and no easy way to report what happened to first line IT support. 12 | 13 | In my opinion when an unhandled exception occurs, an application should do the following: 14 | 15 | * Show the exception message (not the full stack info), 16 | * Offer the user the explicit opportunity to copy the message (for us in an email conversation with support), 17 | * Easy access to the log file folder (makes it easier to attach log file to any email conversation with support), 18 | * Opportunity to terminates the application, 19 | * Opportunity to restart the application, 20 | * Opportunity to continue with current session of the application - obivously this mean possible invalid state etc. 21 | 22 | This approach now targets version 4.6.1 of the Mircosoft .Net framework. This means any unhandled exceptions generated by the TPL & Rx will be propergated to the UI from the finalizer thread when ever the GC runs - this means there is a delay between propergation and observation of any exception. 23 | -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/Collections/RangeObservableCollection.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Collections.ObjectModel; 3 | using System.Collections.Specialized; 4 | using System.Linq; 5 | using System.Windows.Data; 6 | 7 | namespace Simple.Wpf.Exceptions.Collections 8 | { 9 | using Extensions; 10 | 11 | public sealed class RangeObservableCollection : ObservableCollection 12 | { 13 | private bool _suppressNotification; 14 | 15 | public override event NotifyCollectionChangedEventHandler CollectionChanged; 16 | 17 | protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) 18 | { 19 | if (!_suppressNotification) 20 | { 21 | var handlers = CollectionChanged; 22 | if (handlers != null) 23 | { 24 | foreach (var handler in handlers.GetInvocationList().Cast()) 25 | { 26 | if (handler.Target is CollectionView) 27 | { 28 | ((CollectionView)handler.Target).Refresh(); 29 | } 30 | else 31 | { 32 | handler(this, e); 33 | } 34 | } 35 | } 36 | } 37 | } 38 | 39 | public void AddRange(IEnumerable items) 40 | { 41 | _suppressNotification = true; 42 | 43 | var array = items.ToArray(); 44 | array.ForEach(Add); 45 | 46 | _suppressNotification = false; 47 | OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, array, array.Length)); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/Views/MainWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | namespace Simple.Wpf.Exceptions.Views 2 | { 3 | using System; 4 | using System.Reactive; 5 | using System.Reactive.Linq; 6 | using System.Reactive.Threading.Tasks; 7 | using MahApps.Metro.Controls; 8 | using MahApps.Metro.Controls.Dialogs; 9 | using Services; 10 | 11 | public partial class MainWindow : MetroWindow 12 | { 13 | private readonly IDisposable _disposable; 14 | 15 | public MainWindow(IMessageService messageService, ISchedulerService schedulerService) 16 | { 17 | InitializeComponent(); 18 | 19 | _disposable = messageService.Show 20 | // Delay to make sure there is time for the animations 21 | .Delay(TimeSpan.FromMilliseconds(250), schedulerService.TaskPool) 22 | .ObserveOn(schedulerService.Dispatcher) 23 | .Select(x => new MessageDialog(x)) 24 | .SelectMany(ShowDialogAsync, (x, y) => x) 25 | .Subscribe(); 26 | 27 | Closed += HandleClosed; 28 | } 29 | 30 | private void HandleClosed(object sender, EventArgs e) 31 | { 32 | _disposable.Dispose(); 33 | } 34 | 35 | private IObservable ShowDialogAsync(MessageDialog dialog) 36 | { 37 | var settings = new MetroDialogSettings 38 | { 39 | AnimateShow = true, 40 | AnimateHide = true, 41 | ColorScheme = MetroDialogColorScheme.Accented 42 | }; 43 | 44 | return this.ShowMetroDialogAsync(dialog, settings) 45 | .ToObservable() 46 | .SelectMany(x => dialog.CloseableContent.Closed, (x, y) => x) 47 | .SelectMany(x => this.HideMetroDialogAsync(dialog).ToObservable(), (x, y) => x); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/Services/MessageService.cs: -------------------------------------------------------------------------------- 1 | namespace Simple.Wpf.Exceptions.Services 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Reactive.Disposables; 7 | using System.Reactive.Linq; 8 | using System.Reactive.Subjects; 9 | using Extensions; 10 | using Models; 11 | using ViewModels; 12 | 13 | public sealed class MessageService : DisposableObject, IMessageService 14 | { 15 | private readonly Subject _show; 16 | private readonly Queue _waitingMessages = new Queue(); 17 | 18 | private readonly object _sync = new object(); 19 | 20 | public MessageService() 21 | { 22 | _show = new Subject() 23 | .DisposeWith(this); 24 | 25 | Disposable.Create(() => _waitingMessages.Clear()) 26 | .DisposeWith(this); 27 | } 28 | 29 | public void Post(string header, ICloseableViewModel viewModel) 30 | { 31 | var newMessage = new Message(header, viewModel); 32 | 33 | newMessage.ViewModel.Closed 34 | .Take(1) 35 | .Subscribe(x => 36 | { 37 | Message nextMessage = null; 38 | lock(_sync) 39 | { 40 | _waitingMessages.Dequeue(); 41 | 42 | if (_waitingMessages.Any()) 43 | { 44 | nextMessage = _waitingMessages.Peek(); 45 | } 46 | } 47 | 48 | if (nextMessage != null) 49 | { 50 | _show.OnNext(nextMessage); 51 | } 52 | }); 53 | 54 | bool show; 55 | lock(_sync) 56 | { 57 | _waitingMessages.Enqueue(newMessage); 58 | show = _waitingMessages.Count == 1; 59 | } 60 | 61 | if (show) 62 | { 63 | _show.OnNext(newMessage); 64 | } 65 | } 66 | 67 | public IObservable Show => _show; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/ViewModels/ChromeViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Simple.Wpf.Exceptions.Commands; 3 | using Simple.Wpf.Exceptions.Extensions; 4 | using Simple.Wpf.Exceptions.Services; 5 | 6 | namespace Simple.Wpf.Exceptions.ViewModels 7 | { 8 | public sealed class ChromeViewModel : BaseViewModel, IChromeViewModel 9 | { 10 | private OverlayViewModel _overlay; 11 | 12 | public ChromeViewModel(IMainViewModel main, IOverlayService overlayService) 13 | { 14 | Main = main; 15 | 16 | overlayService.Show 17 | .Subscribe(UpdateOverlay) 18 | .DisposeWith(this); 19 | 20 | CloseOverlayCommand = ReactiveCommand.Create() 21 | .DisposeWith(this); 22 | 23 | CloseOverlayCommand.Subscribe(x => ClearOverlay()) 24 | .DisposeWith(this); 25 | } 26 | 27 | public IMainViewModel Main { get; } 28 | 29 | public ReactiveCommand CloseOverlayCommand { get; } 30 | 31 | public bool HasOverlay => _overlay != null; 32 | 33 | public string OverlayHeader => _overlay != null ? _overlay.Header : string.Empty; 34 | 35 | public BaseViewModel Overlay => _overlay?.ViewModel; 36 | 37 | private void ClearOverlay() 38 | { 39 | using (_overlay.Lifetime) 40 | { 41 | UpdateOverlayImpl(null); 42 | } 43 | } 44 | 45 | private void UpdateOverlay(OverlayViewModel overlay) 46 | { 47 | using (SuspendNotifications()) 48 | { 49 | if (_overlay != null) ClearOverlay(); 50 | 51 | UpdateOverlayImpl(overlay); 52 | } 53 | } 54 | 55 | private void UpdateOverlayImpl(OverlayViewModel overlay) 56 | { 57 | _overlay = overlay; 58 | 59 | OnPropertyChanged(() => HasOverlay); 60 | OnPropertyChanged(() => Overlay); 61 | OnPropertyChanged(() => OverlayHeader); 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/ViewModels/CloseableViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace Simple.Wpf.Exceptions.ViewModels 2 | { 3 | using System; 4 | using System.Reactive; 5 | using System.Reactive.Linq; 6 | using System.Reactive.Subjects; 7 | using Commands; 8 | using Extensions; 9 | 10 | public abstract class CloseableViewModel : BaseViewModel, ICloseableViewModel 11 | { 12 | private readonly Subject _closed; 13 | private readonly Subject _denied; 14 | private readonly Subject _confirmed; 15 | 16 | protected CloseableViewModel() 17 | { 18 | _closed = new Subject() 19 | .DisposeWith(this); 20 | 21 | _denied = new Subject() 22 | .DisposeWith(this); 23 | 24 | _confirmed = new Subject() 25 | .DisposeWith(this); 26 | 27 | CancelCommand = ReactiveCommand.Create() 28 | .DisposeWith(this); 29 | 30 | CancelCommand.ActivateGestures() 31 | .Subscribe(x => _closed.OnNext(Unit.Default)) 32 | .DisposeWith(this); 33 | 34 | ConfirmCommand = ReactiveCommand.Create(InitialiseCanConfirm()) 35 | .DisposeWith(this); 36 | 37 | ConfirmCommand.ActivateGestures() 38 | .Subscribe(x => 39 | { 40 | _confirmed.OnNext(Unit.Default); 41 | _closed.OnNext(Unit.Default); 42 | }) 43 | .DisposeWith(this); 44 | 45 | DenyCommand = ReactiveCommand.Create(InitialiseCanDeny()) 46 | .DisposeWith(this); 47 | 48 | DenyCommand.ActivateGestures() 49 | .Subscribe(x => 50 | { 51 | _denied.OnNext(Unit.Default); 52 | _closed.OnNext(Unit.Default); 53 | }) 54 | .DisposeWith(this); 55 | } 56 | 57 | public IObservable Closed => _closed; 58 | public IObservable Denied => _denied; 59 | public IObservable Confirmed => _confirmed; 60 | public ReactiveCommand CancelCommand { get; } 61 | public ReactiveCommand ConfirmCommand { get; protected set; } 62 | public ReactiveCommand DenyCommand { get; protected set; } 63 | 64 | protected virtual IObservable InitialiseCanConfirm() 65 | { 66 | return Observable.Return(true); 67 | } 68 | 69 | protected virtual IObservable InitialiseCanDeny() 70 | { 71 | return Observable.Return(true); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/Extensions/ObservableExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Simple.Wpf.Exceptions.Extensions 2 | { 3 | using System; 4 | using System.Reactive; 5 | using System.Reactive.Concurrency; 6 | using System.Reactive.Linq; 7 | using System.Threading; 8 | using Commands; 9 | using Services; 10 | 11 | public static class ObservableExtensions 12 | { 13 | public static IGestureService GestureService; 14 | 15 | public static IObservable AsUnit(this IObservable observable) 16 | { 17 | return observable.Select(x => Unit.Default); 18 | } 19 | 20 | public static ReactiveCommand ToCommand(this IObservable canExecute) 21 | { 22 | return ReactiveCommand.Create(canExecute); 23 | } 24 | 25 | public static IObservable ActivateGestures(this IObservable observable) 26 | { 27 | if (GestureService == null) 28 | { 29 | throw new Exception("GestureService has not been initialised"); 30 | } 31 | 32 | return observable.Do(x => GestureService.SetBusy()); 33 | } 34 | 35 | public static IDisposable SafeSubscribe(this IObservable observable, Action onNext, Action onError, IScheduler scheduler) 36 | { 37 | return observable.Subscribe(x => OnNextInvoke(onNext, x, scheduler), onError); 38 | } 39 | 40 | public static IDisposable SafeSubscribe(this IObservable observable, Action onNext, Action onCompleted, IScheduler scheduler) 41 | { 42 | return observable.Subscribe(x => OnNextInvoke(onNext, x, scheduler), onCompleted); 43 | } 44 | 45 | public static IDisposable SafeSubscribe(this IObservable observable, Action onNext, IScheduler scheduler) 46 | { 47 | return observable.Subscribe(x => OnNextInvoke(onNext, x, scheduler)); 48 | } 49 | 50 | public static void SafeSubscribe(this IObservable observable, Action onNext, Action onCompleted, CancellationToken token, IScheduler scheduler) 51 | { 52 | observable.Subscribe(x => OnNextInvoke(onNext, x, scheduler), onCompleted, token); 53 | } 54 | 55 | public static void SafeSubscribe(this IObservable observable, Action onNext, Action onError, Action onCompleted, CancellationToken token, IScheduler scheduler) 56 | { 57 | observable.Subscribe(x => OnNextInvoke(onNext, x, scheduler), onError, onCompleted, token); 58 | } 59 | 60 | private static void OnNextInvoke(Action onNext, T instance, IScheduler scheduler) 61 | { 62 | try 63 | { 64 | onNext(instance); 65 | } 66 | catch (Exception exn) 67 | { 68 | scheduler.Schedule(() => { throw exn; }); 69 | } 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/ViewModels/MainViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace Simple.Wpf.Exceptions.ViewModels 2 | { 3 | using System; 4 | using System.Reactive.Linq; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Commands; 8 | using Extensions; 9 | using Services; 10 | 11 | public sealed class MainViewModel : BaseViewModel, IMainViewModel 12 | { 13 | public MainViewModel(ISchedulerService schedulerService) 14 | { 15 | ThrowFromUiThreadCommand = ReactiveCommand.Create() 16 | .DisposeWith(this); 17 | 18 | ThrowFromTaskCommand = ReactiveCommand.Create() 19 | .DisposeWith(this); 20 | 21 | ThrowFromRxCommand = ReactiveCommand.Create() 22 | .DisposeWith(this); 23 | 24 | ThrowFromUiThreadCommand 25 | .ActivateGestures() 26 | .SafeSubscribe(x => 27 | { 28 | Logger.Info("ThrowFromUiThreadCommand executing..."); 29 | throw new Exception(x + " - thrown from UI thread."); 30 | }, schedulerService.Dispatcher) 31 | .DisposeWith(this); 32 | 33 | ThrowFromTaskCommand 34 | .ActivateGestures() 35 | .Subscribe(x => 36 | { 37 | Logger.Info("ThrowFromTaskCommand executing..."); 38 | 39 | Task.Factory.StartNew(() => 40 | { 41 | Thread.Sleep(1000); 42 | 43 | throw new Exception(x + " - thrown from Task StartNew."); 44 | }, TaskCreationOptions.LongRunning); 45 | }) 46 | .DisposeWith(this); 47 | 48 | 49 | ThrowFromRxCommand 50 | .ActivateGestures() 51 | .Subscribe(x => 52 | { 53 | Logger.Info("ThrowFromRxCommand executing..."); 54 | 55 | Observable.Start(() => 56 | { 57 | Thread.Sleep(1000); 58 | 59 | throw new Exception(x + " - thrown from Rx Start."); 60 | }, schedulerService.TaskPool) 61 | .Take(1) 62 | .Subscribe(); 63 | }) 64 | .DisposeWith(this); 65 | } 66 | 67 | public ReactiveCommand ThrowFromUiThreadCommand { get; } 68 | 69 | public ReactiveCommand ThrowFromTaskCommand { get; } 70 | 71 | public ReactiveCommand ThrowFromRxCommand { get; } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace Simple.Wpf.Exceptions.Properties { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Simple.Wpf.Exceptions.Properties.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/Commands/ReactiveCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Simple.Wpf.Exceptions.Commands 2 | { 3 | using System; 4 | using System.Reactive.Linq; 5 | using System.Reactive.Subjects; 6 | using System.Windows.Input; 7 | using NLog; 8 | 9 | public sealed class ReactiveCommand : ReactiveCommand 10 | { 11 | private ReactiveCommand(IObservable canExecute) 12 | : base(canExecute.StartWith(false)) 13 | { 14 | } 15 | 16 | public new static ReactiveCommand Create() 17 | { 18 | return ReactiveCommand.Create(Observable.Return(true).StartWith(true)); 19 | } 20 | 21 | public new static ReactiveCommand Create(IObservable canExecute) 22 | { 23 | return ReactiveCommand.Create(canExecute); 24 | } 25 | } 26 | 27 | public class ReactiveCommand : IObservable, ICommand, IDisposable 28 | { 29 | private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); 30 | 31 | private readonly Subject _execute; 32 | private readonly IDisposable _canDisposable; 33 | private bool _currentCanExecute; 34 | 35 | protected ReactiveCommand(IObservable canExecute) 36 | { 37 | _canDisposable = canExecute.Subscribe(x => 38 | { 39 | _currentCanExecute = x; 40 | CommandManager.InvalidateRequerySuggested(); 41 | }); 42 | 43 | _execute = new Subject(); 44 | } 45 | 46 | public static ReactiveCommand Create() 47 | { 48 | return new ReactiveCommand(Observable.Return(true)); 49 | } 50 | 51 | public static ReactiveCommand Create(IObservable canExecute) 52 | { 53 | return new ReactiveCommand(canExecute); 54 | } 55 | 56 | public void Dispose() 57 | { 58 | using (Duration.Measure(Logger, "Dispose - " + GetType().Name)) 59 | { 60 | _canDisposable.Dispose(); 61 | 62 | _execute.OnCompleted(); 63 | _execute.Dispose(); 64 | } 65 | } 66 | 67 | public virtual void Execute(object parameter) 68 | { 69 | var typedParameter = parameter is T ? (T)parameter : default(T); 70 | 71 | if (CanExecute(typedParameter)) 72 | { 73 | _execute.OnNext(typedParameter); 74 | } 75 | } 76 | 77 | public virtual bool CanExecute(object parameter) 78 | { 79 | return _currentCanExecute; 80 | } 81 | 82 | public event EventHandler CanExecuteChanged 83 | { 84 | add { CommandManager.RequerySuggested += value; } 85 | remove { CommandManager.RequerySuggested -= value; } 86 | } 87 | 88 | public IDisposable Subscribe(IObserver observer) 89 | { 90 | return _execute.Subscribe(observer.OnNext, observer.OnError, observer.OnCompleted); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/app.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions.Tests/app.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/BootStrapper.cs: -------------------------------------------------------------------------------- 1 | namespace Simple.Wpf.Exceptions 2 | { 3 | using System; 4 | using System.Diagnostics; 5 | using System.Reflection; 6 | using Autofac; 7 | using Autofac.Core; 8 | using Services; 9 | using ViewModels; 10 | 11 | public static class BootStrapper 12 | { 13 | private static ILifetimeScope _rootScope; 14 | private static IChromeViewModel _chromeViewModel; 15 | 16 | public static IViewModel RootVisual 17 | { 18 | get 19 | { 20 | if (_rootScope == null) 21 | { 22 | Start(); 23 | } 24 | 25 | _chromeViewModel = _rootScope.Resolve(); 26 | return _chromeViewModel; 27 | } 28 | } 29 | 30 | public static void Start() 31 | { 32 | if (_rootScope != null) 33 | { 34 | return; 35 | } 36 | 37 | var builder = new ContainerBuilder(); 38 | var assemblies = new[] { Assembly.GetExecutingAssembly() }; 39 | 40 | builder.RegisterAssemblyTypes(assemblies) 41 | .Where(t => typeof(IService).IsAssignableFrom(t)) 42 | .SingleInstance() 43 | .AsImplementedInterfaces(); 44 | 45 | builder.RegisterAssemblyTypes(assemblies) 46 | .Where(t => typeof(IViewModel).IsAssignableFrom(t) && !typeof(ITransientViewModel).IsAssignableFrom(t)) 47 | .AsImplementedInterfaces(); 48 | 49 | // several view model instances are transitory and created on the fly, if these are tracked by the container then they 50 | // won't be disposed of in a timely manner 51 | 52 | builder.RegisterAssemblyTypes(assemblies) 53 | .Where(t => typeof(IViewModel).IsAssignableFrom(t)) 54 | .Where(t => 55 | { 56 | var isAssignable = typeof(ITransientViewModel).IsAssignableFrom(t); 57 | if (isAssignable) 58 | { 59 | Debug.WriteLine("Transient view model - " + t.Name); 60 | } 61 | 62 | return isAssignable; 63 | }) 64 | .AsImplementedInterfaces() 65 | .ExternallyOwned(); 66 | 67 | _rootScope = builder.Build(); 68 | } 69 | 70 | public static void Stop() 71 | { 72 | _rootScope.Dispose(); 73 | } 74 | 75 | public static T Resolve() 76 | { 77 | if (_rootScope == null) 78 | { 79 | throw new Exception("Bootstrapper hasn't been started!"); 80 | } 81 | 82 | return _rootScope.Resolve(new Parameter[0]); 83 | } 84 | 85 | public static T Resolve(Parameter[] parameters) 86 | { 87 | if (_rootScope == null) 88 | { 89 | throw new Exception("Bootstrapper hasn't been started!"); 90 | } 91 | 92 | return _rootScope.Resolve(parameters); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions.Tests/MessageServiceFixtures.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Moq; 5 | using NUnit.Framework; 6 | using Simple.Wpf.Exceptions.Models; 7 | using Simple.Wpf.Exceptions.Services; 8 | using Simple.Wpf.Exceptions.ViewModels; 9 | 10 | namespace Simple.Wpf.Exceptions.Tests 11 | { 12 | using System.Reactive; 13 | using System.Reactive.Subjects; 14 | 15 | [TestFixture] 16 | public sealed class MessageServiceFixtures 17 | { 18 | [Test] 19 | public void posts_message_with_lifetime() 20 | { 21 | // ARRANGE 22 | var closed = new Subject(); 23 | var contentViewModel = new Mock(); 24 | contentViewModel.Setup(x => x.Closed).Returns(closed); 25 | 26 | var service = new MessageService(); 27 | 28 | Message message = null; 29 | service.Show.Subscribe(x => message = x); 30 | 31 | // ACT 32 | service.Post("header 1", contentViewModel.Object); 33 | 34 | // ASSERT 35 | Assert.That(message.Header, Is.EqualTo("header 1")); 36 | Assert.That(message.ViewModel, Is.EqualTo(contentViewModel.Object)); 37 | } 38 | 39 | [Test] 40 | public void posts_message_without_lifetime() 41 | { 42 | // ARRANGE 43 | var closed = new Subject(); 44 | var contentViewModel = new Mock(); 45 | contentViewModel.Setup(x => x.Closed).Returns(closed); 46 | 47 | var service = new MessageService(); 48 | 49 | Message message = null; 50 | service.Show.Subscribe(x => message = x); 51 | 52 | // ACT 53 | service.Post("header 1", contentViewModel.Object); 54 | 55 | // ASSERT 56 | Assert.That(message.Header, Is.EqualTo("header 1")); 57 | Assert.That(message.ViewModel, Is.EqualTo(contentViewModel.Object)); 58 | } 59 | 60 | [Test] 61 | public void posts_mulitple_messages() 62 | { 63 | // ARRANGE 64 | var closed1 = new Subject(); 65 | var contentViewModel1 = new Mock(); 66 | contentViewModel1.Setup(x => x.Closed).Returns(closed1); 67 | 68 | var closed2 = new Subject(); 69 | var contentViewModel2 = new Mock(); 70 | contentViewModel2.Setup(x => x.Closed).Returns(closed2); 71 | 72 | var service = new MessageService(); 73 | 74 | var messages = new List(); 75 | service.Show.Subscribe(x => messages.Add(x)); 76 | 77 | service.Post("header 1", contentViewModel1.Object); 78 | service.Post("header 2", contentViewModel2.Object); 79 | 80 | // ACT 81 | closed1.OnNext(Unit.Default); 82 | 83 | // ASSERT 84 | Assert.That(messages.Count(x => x.Header == "header 1") == 1, Is.True); 85 | Assert.That(messages.Count(x => x.Header == "header 2") == 1, Is.True); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/ViewModels/ExceptionViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace Simple.Wpf.Exceptions.ViewModels 2 | { 3 | using System; 4 | using System.Reactive.Linq; 5 | using Commands; 6 | using Extensions; 7 | using Services; 8 | 9 | public sealed class ExceptionViewModel : CloseableViewModel, IExceptionViewModel 10 | { 11 | private readonly Exception _exception; 12 | private readonly IApplicationService _applicationService; 13 | 14 | public ExceptionViewModel(Exception exception, IApplicationService applicationService) 15 | { 16 | _exception = exception; 17 | _applicationService = applicationService; 18 | 19 | OpenLogFolderCommand = ReactiveCommand.Create(Observable.Return(_applicationService.LogFolder != null)) 20 | .DisposeWith(this); 21 | 22 | CopyCommand = ReactiveCommand.Create(Observable.Return(exception != null)) 23 | .DisposeWith(this); 24 | 25 | ContinueCommand = ReactiveCommand.Create() 26 | .DisposeWith(this); 27 | 28 | ExitCommand = ReactiveCommand.Create() 29 | .DisposeWith(this); 30 | 31 | RestartCommand = ReactiveCommand.Create() 32 | .DisposeWith(this); 33 | 34 | OpenLogFolderCommand.ActivateGestures() 35 | .Subscribe(x => OpenLogFolder()) 36 | .DisposeWith(this); 37 | 38 | CopyCommand.ActivateGestures() 39 | .Subscribe(x => Copy()) 40 | .DisposeWith(this); 41 | 42 | ContinueCommand.ActivateGestures() 43 | .Subscribe(x => Continue()) 44 | .DisposeWith(this); 45 | 46 | ExitCommand 47 | .ActivateGestures() 48 | .Subscribe(x => Exit()) 49 | .DisposeWith(this); 50 | 51 | RestartCommand 52 | .ActivateGestures() 53 | .Subscribe(x => Restart()) 54 | .DisposeWith(this); 55 | 56 | Closed.Take(1) 57 | .Subscribe(x => 58 | { 59 | // Force all other potential exceptions to be realized 60 | // from the Finalizer thread to surface to the UI 61 | GC.Collect(2, GCCollectionMode.Forced); 62 | GC.WaitForPendingFinalizers(); 63 | }) 64 | .DisposeWith(this); 65 | } 66 | 67 | public ReactiveCommand CopyCommand { get; } 68 | 69 | public ReactiveCommand OpenLogFolderCommand { get; } 70 | 71 | public ReactiveCommand ContinueCommand { get; } 72 | 73 | public ReactiveCommand ExitCommand { get; } 74 | 75 | public ReactiveCommand RestartCommand { get; } 76 | 77 | public string Message => _exception?.Message; 78 | 79 | private void Copy() 80 | { 81 | _applicationService.CopyToClipboard(_exception.ToString()); 82 | } 83 | 84 | private void OpenLogFolder() 85 | { 86 | _applicationService.OpenFolder(_applicationService.LogFolder); 87 | } 88 | 89 | private void Exit() 90 | { 91 | _applicationService.Exit(); 92 | } 93 | 94 | private void Restart() 95 | { 96 | _applicationService.Restart(); 97 | } 98 | 99 | private void Continue() 100 | { 101 | ConfirmCommand.Execute(null); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | build/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | 25 | # Visual Studo 2015 cache/options directory 26 | .vs/ 27 | 28 | # MSTest test Results 29 | [Tt]est[Rr]esult*/ 30 | [Bb]uild[Ll]og.* 31 | 32 | # NUNIT 33 | *.VisualState.xml 34 | TestResult.xml 35 | 36 | # Build Results of an ATL Project 37 | [Dd]ebugPS/ 38 | [Rr]eleasePS/ 39 | dlldata.c 40 | 41 | *_i.c 42 | *_p.c 43 | *_i.h 44 | *.ilk 45 | *.meta 46 | *.obj 47 | *.pch 48 | *.pdb 49 | *.pgc 50 | *.pgd 51 | *.rsp 52 | *.sbr 53 | *.tlb 54 | *.tli 55 | *.tlh 56 | *.tmp 57 | *.tmp_proj 58 | *.log 59 | *.vspscc 60 | *.vssscc 61 | .builds 62 | *.pidb 63 | *.svclog 64 | *.scc 65 | 66 | # Chutzpah Test files 67 | _Chutzpah* 68 | 69 | # Visual C++ cache files 70 | ipch/ 71 | *.aps 72 | *.ncb 73 | *.opensdf 74 | *.sdf 75 | *.cachefile 76 | 77 | # Visual Studio profiler 78 | *.psess 79 | *.vsp 80 | *.vspx 81 | 82 | # TFS 2012 Local Workspace 83 | $tf/ 84 | 85 | # Guidance Automation Toolkit 86 | *.gpState 87 | 88 | # ReSharper is a .NET coding add-in 89 | _ReSharper*/ 90 | *.[Rr]e[Ss]harper 91 | *.DotSettings.user 92 | 93 | # JustCode is a .NET coding addin-in 94 | .JustCode 95 | 96 | # TeamCity is a build add-in 97 | _TeamCity* 98 | 99 | # DotCover is a Code Coverage Tool 100 | *.dotCover 101 | 102 | # NCrunch 103 | _NCrunch_* 104 | .*crunch*.local.xml 105 | 106 | # MightyMoose 107 | *.mm.* 108 | AutoTest.Net/ 109 | 110 | # Web workbench (sass) 111 | .sass-cache/ 112 | 113 | # Installshield output folder 114 | [Ee]xpress/ 115 | 116 | # DocProject is a documentation generator add-in 117 | DocProject/buildhelp/ 118 | DocProject/Help/*.HxT 119 | DocProject/Help/*.HxC 120 | DocProject/Help/*.hhc 121 | DocProject/Help/*.hhk 122 | DocProject/Help/*.hhp 123 | DocProject/Help/Html2 124 | DocProject/Help/html 125 | 126 | # Click-Once directory 127 | publish/ 128 | 129 | # Publish Web Output 130 | *.[Pp]ublish.xml 131 | *.azurePubxml 132 | # TODO: Comment the next line if you want to checkin your web deploy settings 133 | # but database connection strings (with potential passwords) will be unencrypted 134 | *.pubxml 135 | *.publishproj 136 | 137 | # NuGet Packages 138 | *.nupkg 139 | # The packages folder can be ignored because of Package Restore 140 | **/packages/* 141 | # except build/, which is used as an MSBuild target. 142 | !**/packages/build/ 143 | # Uncomment if necessary however generally it will be regenerated when needed 144 | #!**/packages/repositories.config 145 | 146 | # Windows Azure Build Output 147 | csx/ 148 | *.build.csdef 149 | 150 | # Windows Store app package directory 151 | AppPackages/ 152 | 153 | # Others 154 | *.[Cc]ache 155 | ClientBin/ 156 | [Ss]tyle[Cc]op.* 157 | ~$* 158 | *~ 159 | *.dbmdl 160 | *.dbproj.schemaview 161 | *.pfx 162 | *.publishsettings 163 | node_modules/ 164 | bower_components/ 165 | 166 | # RIA/Silverlight projects 167 | Generated_Code/ 168 | 169 | # Backup & report files from converting an old project file 170 | # to a newer Visual Studio version. Backup files are not needed, 171 | # because we have git ;-) 172 | _UpgradeReport_Files/ 173 | Backup*/ 174 | UpgradeLog*.XML 175 | UpgradeLog*.htm 176 | 177 | # SQL Server files 178 | *.mdf 179 | *.ldf 180 | 181 | # Business Intelligence projects 182 | *.rdl.data 183 | *.bim.layout 184 | *.bim_*.settings 185 | 186 | # Microsoft Fakes 187 | FakesAssemblies/ 188 | 189 | # Node.js Tools for Visual Studio 190 | .ntvs_analysis.dat 191 | 192 | # Visual Studio 6 build log 193 | *.plg 194 | 195 | # Visual Studio 6 workspace options file 196 | *.opt 197 | -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions.Tests/MainViewModelFixtures.cs: -------------------------------------------------------------------------------- 1 | namespace Simple.Wpf.Exceptions.Tests 2 | { 3 | using System; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Microsoft.Reactive.Testing; 7 | using Moq; 8 | using NUnit.Framework; 9 | using Services; 10 | using ViewModels; 11 | 12 | [TestFixture] 13 | public sealed class MainViewModelFixtures 14 | { 15 | private TestScheduler _testScheduler; 16 | private ISchedulerService _schedulerService; 17 | 18 | [SetUp] 19 | public void Setup() 20 | { 21 | _testScheduler = new TestScheduler(); 22 | _schedulerService = new MockSchedulerService(_testScheduler); 23 | 24 | var gestureService = new Mock(); 25 | gestureService.Setup(x => x.SetBusy()); 26 | 27 | Extensions.ObservableExtensions.GestureService = gestureService.Object; 28 | } 29 | 30 | [Test] 31 | public void throws_exception_on_ui_thread() 32 | { 33 | // ARRANGE 34 | var exceptionText = "This is the exception message!"; 35 | var expectedResult = "This is the exception message! - thrown from UI thread."; 36 | 37 | var viewModel = new MainViewModel(_schedulerService); 38 | 39 | // ACT 40 | Exception thrownException = null; 41 | try 42 | { 43 | viewModel.ThrowFromUiThreadCommand.Execute(exceptionText); 44 | 45 | _testScheduler.AdvanceBy(TimeSpan.FromMilliseconds(100)); 46 | } 47 | catch (Exception exn) 48 | { 49 | thrownException = exn; 50 | } 51 | 52 | // ASSERT 53 | Assert.That(thrownException, Is.Not.Null); 54 | Assert.That(thrownException.Message, Is.EqualTo(expectedResult)); 55 | } 56 | 57 | [Test] 58 | public void throws_exception_from_task_startnew() 59 | { 60 | // ARRANGE 61 | var exceptionText = "This is the exception message!"; 62 | var expectedResult = "This is the exception message! - thrown from Task StartNew."; 63 | 64 | var viewModel = new MainViewModel(_schedulerService); 65 | 66 | Exception thrownException = null; 67 | TaskScheduler.UnobservedTaskException += (s, e) => 68 | { 69 | thrownException = e.Exception.InnerException; 70 | }; 71 | 72 | // ACT 73 | viewModel.ThrowFromTaskCommand.Execute(exceptionText); 74 | 75 | // Hack to test this... 76 | Thread.Sleep(5000); 77 | 78 | GC.Collect(); 79 | GC.WaitForPendingFinalizers(); 80 | 81 | // ASSERT 82 | Assert.That(thrownException, Is.Not.Null); 83 | Assert.That(thrownException.Message, Is.EqualTo(expectedResult)); 84 | } 85 | 86 | [Test] 87 | public void throws_exception_from_rx_start() 88 | { 89 | // ARRANGE 90 | var exceptionText = "This is the exception message!"; 91 | var expectedResult = "This is the exception message! - thrown from Rx Start."; 92 | 93 | var viewModel = new MainViewModel(_schedulerService); 94 | 95 | Exception thrownException = null; 96 | 97 | // ACT 98 | try 99 | { 100 | viewModel.ThrowFromRxCommand.Execute(exceptionText); 101 | 102 | _testScheduler.AdvanceBy(TimeSpan.FromSeconds(1)); 103 | } 104 | catch(Exception exn) 105 | { 106 | thrownException = exn; 107 | } 108 | 109 | // ASSERT 110 | Assert.That(thrownException, Is.Not.Null); 111 | Assert.That(thrownException.Message, Is.EqualTo(expectedResult)); 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/ViewModels/BaseViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace Simple.Wpf.Exceptions.ViewModels 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.ComponentModel; 6 | using System.Linq.Expressions; 7 | using System.Reactive.Disposables; 8 | using Extensions; 9 | using Helpers; 10 | using Models; 11 | 12 | public abstract class BaseViewModel : DisposableObject, IViewModel 13 | { 14 | public event PropertyChangedEventHandler PropertyChanged; 15 | 16 | private sealed class SuspendedNotifications : IDisposable 17 | { 18 | private readonly BaseViewModel _target; 19 | private readonly HashSet _properties = new HashSet(); 20 | private int _refCount; 21 | 22 | public SuspendedNotifications(BaseViewModel target) 23 | { 24 | _target = target; 25 | } 26 | 27 | public void Add(string propertyName) 28 | { 29 | _properties.Add(propertyName); 30 | } 31 | 32 | public IDisposable AddRef() 33 | { 34 | ++_refCount; 35 | return Disposable.Create(() => 36 | { 37 | if (--_refCount == 0) 38 | { 39 | Dispose(); 40 | } 41 | }); 42 | } 43 | 44 | public void Dispose() 45 | { 46 | _target._suspendedNotifications = null; 47 | _properties.ForEach(x => _target.OnPropertyChanged(x)); 48 | } 49 | } 50 | 51 | private static readonly PropertyChangedEventArgs EmptyChangeArgs = new PropertyChangedEventArgs(string.Empty); 52 | private static readonly IDictionary ChangedProperties = new Dictionary(); 53 | 54 | private SuspendedNotifications _suspendedNotifications; 55 | 56 | public IDisposable SuspendNotifications() 57 | { 58 | if (_suspendedNotifications == null) 59 | { 60 | _suspendedNotifications = new SuspendedNotifications(this); 61 | } 62 | 63 | return _suspendedNotifications.AddRef(); 64 | } 65 | 66 | protected virtual void OnPropertyChanged(Expression> expression) 67 | { 68 | OnPropertyChanged(ExpressionHelper.Name(expression)); 69 | } 70 | 71 | protected virtual void OnPropertyChanged() 72 | { 73 | OnPropertyChanged(null); 74 | } 75 | 76 | protected virtual void OnPropertyChanged(string propertyName) 77 | { 78 | if (_suspendedNotifications != null) 79 | { 80 | _suspendedNotifications.Add(propertyName); 81 | } 82 | else 83 | { 84 | var handler = PropertyChanged; 85 | if (handler != null) 86 | { 87 | if (propertyName == null) 88 | { 89 | handler(this, EmptyChangeArgs); 90 | } 91 | else 92 | { 93 | PropertyChangedEventArgs args; 94 | if (!ChangedProperties.TryGetValue(propertyName, out args)) 95 | { 96 | args = new PropertyChangedEventArgs(propertyName); 97 | ChangedProperties.Add(propertyName, args); 98 | } 99 | 100 | handler(this, args); 101 | } 102 | } 103 | } 104 | } 105 | 106 | protected virtual bool SetPropertyAndNotify(ref T existingValue, T newValue, Expression> expression) 107 | { 108 | if (EqualityComparer.Default.Equals(existingValue, newValue)) 109 | { 110 | return false; 111 | } 112 | 113 | existingValue = newValue; 114 | OnPropertyChanged(expression); 115 | 116 | return true; 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/App.xaml.cs: -------------------------------------------------------------------------------- 1 | namespace Simple.Wpf.Exceptions 2 | { 3 | using System; 4 | using System.Diagnostics; 5 | using System.Reactive.Concurrency; 6 | using System.Reactive.Disposables; 7 | using System.Reactive.Linq; 8 | using System.Threading.Tasks; 9 | using System.Windows; 10 | using System.Windows.Media; 11 | using System.Windows.Threading; 12 | using Autofac; 13 | using Autofac.Core; 14 | using Helpers; 15 | using NLog; 16 | using Services; 17 | using ViewModels; 18 | using Views; 19 | 20 | public partial class App : Application 21 | { 22 | private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); 23 | 24 | private readonly CompositeDisposable _disposable; 25 | private IMessageService _messageService; 26 | private ISchedulerService _schedulerService; 27 | 28 | public App() 29 | { 30 | #if DEBUG 31 | LogHelper.ReconfigureLoggerToLevel(LogLevel.Debug); 32 | #endif 33 | 34 | AppDomain.CurrentDomain.UnhandledException += CurrentDomainOnUnhandledException; 35 | Application.Current.DispatcherUnhandledException += DispatcherOnUnhandledException; 36 | TaskScheduler.UnobservedTaskException += TaskSchedulerOnUnobservedTaskException; 37 | 38 | _disposable = new CompositeDisposable(); 39 | } 40 | 41 | protected override void OnStartup(StartupEventArgs e) 42 | { 43 | Logger.Info("Starting"); 44 | Logger.Info("Dispatcher managed thread identifier = {0}", System.Threading.Thread.CurrentThread.ManagedThreadId); 45 | 46 | Logger.Info("WPF rendering capability (tier) = {0}", RenderCapability.Tier / 0x10000); 47 | RenderCapability.TierChanged += (s, a) => Logger.Info("WPF rendering capability (tier) = {0}", RenderCapability.Tier / 0x10000); 48 | 49 | base.OnStartup(e); 50 | 51 | BootStrapper.Start(); 52 | 53 | _messageService = BootStrapper.Resolve(); 54 | _schedulerService = BootStrapper.Resolve(); 55 | Extensions.ObservableExtensions.GestureService = BootStrapper.Resolve(); 56 | 57 | var window = new MainWindow(_messageService, _schedulerService); 58 | 59 | // The window has to be created before the root visual - all to do with the idling service initialising correctly... 60 | window.DataContext = BootStrapper.RootVisual; 61 | 62 | window.Closed += (s, a) => 63 | { 64 | _disposable.Dispose(); 65 | BootStrapper.Stop(); 66 | }; 67 | 68 | Current.Exit += (s, a) => 69 | { 70 | Logger.Info("Bye Bye!"); 71 | LogManager.Flush(); 72 | }; 73 | 74 | window.Show(); 75 | 76 | #if DEBUG 77 | _disposable.Add(ObserveUiFreeze()); 78 | #endif 79 | Logger.Info("Started"); 80 | } 81 | 82 | private static IDisposable ObserveUiFreeze() 83 | { 84 | var timer = new DispatcherTimer(DispatcherPriority.Normal) 85 | { 86 | Interval = Constants.UiFreezeTimer 87 | }; 88 | 89 | var previous = DateTime.Now; 90 | timer.Tick += (sender, args) => 91 | { 92 | var current = DateTime.Now; 93 | var delta = current - previous; 94 | previous = current; 95 | 96 | if (delta > Constants.UiFreeze) 97 | { 98 | Debug.WriteLine("UI Freeze = {0} ms", delta.TotalMilliseconds); 99 | } 100 | }; 101 | 102 | timer.Start(); 103 | return Disposable.Create(timer.Stop); 104 | } 105 | 106 | private void CurrentDomainOnUnhandledException(object sender, UnhandledExceptionEventArgs args) 107 | { 108 | Logger.Info("Unhandled app domain exception"); 109 | HandleException(args.ExceptionObject as Exception); 110 | } 111 | 112 | private void DispatcherOnUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs args) 113 | { 114 | Logger.Info("Unhandled dispatcher thread exception"); 115 | args.Handled = true; 116 | 117 | HandleException(args.Exception); 118 | } 119 | 120 | private void TaskSchedulerOnUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs args) 121 | { 122 | Logger.Info("Unhandled task exception"); 123 | args.SetObserved(); 124 | 125 | HandleException(args.Exception.GetBaseException()); 126 | } 127 | 128 | private void HandleException(Exception exception) 129 | { 130 | Logger.Error(exception); 131 | 132 | _schedulerService.Dispatcher.Schedule(() => 133 | { 134 | var parameters = new Parameter[] { new NamedParameter("exception", exception) }; 135 | var viewModel = BootStrapper.Resolve(parameters); 136 | 137 | viewModel.Closed 138 | .Take(1) 139 | .Subscribe(x => viewModel.Dispose()); 140 | 141 | _messageService.Post("whoops - something's gone wrong!", viewModel); 142 | }); 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/Properties/Resources.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | text/microsoft-resx 107 | 108 | 109 | 2.0 110 | 111 | 112 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 113 | 114 | 115 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions.Tests/ExceptionViewModelFixtures.cs: -------------------------------------------------------------------------------- 1 | namespace Simple.Wpf.Exceptions.Tests 2 | { 3 | using System; 4 | using Moq; 5 | using NUnit.Framework; 6 | using Services; 7 | using ViewModels; 8 | 9 | [TestFixture] 10 | public sealed class ExceptionViewModelFixtures 11 | { 12 | private Mock _applicationService; 13 | 14 | [SetUp] 15 | public void Setup() 16 | { 17 | _applicationService = new Mock(); 18 | 19 | var gestureService = new Mock(); 20 | gestureService.Setup(x => x.SetBusy()); 21 | 22 | Extensions.ObservableExtensions.GestureService = gestureService.Object; 23 | } 24 | 25 | [Test] 26 | public void message_is_null_when_exception_is_null() 27 | { 28 | // ARRANGE 29 | // ACT 30 | var viewModel = new ExceptionViewModel(null, _applicationService.Object); 31 | 32 | // ASSERT 33 | Assert.That(viewModel.Message, Is.Null); 34 | } 35 | 36 | [Test] 37 | public void message_is_populated_when_exception_is_not_null() 38 | { 39 | // ARRANGE 40 | var message = "This is the message"; 41 | var exception = new Exception(message); 42 | 43 | // ACT 44 | var viewModel = new ExceptionViewModel(exception, _applicationService.Object); 45 | 46 | // ASSERT 47 | Assert.That(viewModel.Message, Is.EqualTo(message)); 48 | } 49 | 50 | [Test] 51 | public void can_not_copy_exception_to_clipboard_when_exception_is_null() 52 | { 53 | // ARRANGE 54 | var viewModel = new ExceptionViewModel(null, _applicationService.Object); 55 | 56 | // ACT 57 | var canExecute = viewModel.CopyCommand.CanExecute(null); 58 | 59 | // ASSERT 60 | Assert.That(canExecute, Is.False); 61 | } 62 | 63 | [Test] 64 | public void can_copy_exception_to_clipboard_when_exception_is_null() 65 | { 66 | // ARRANGE 67 | var message = "This is the message"; 68 | var exception = new Exception(message); 69 | 70 | var viewModel = new ExceptionViewModel(exception, _applicationService.Object); 71 | 72 | // ACT 73 | var canExecute = viewModel.CopyCommand.CanExecute(null); 74 | 75 | // ASSERT 76 | Assert.That(canExecute, Is.True); 77 | } 78 | 79 | [Test] 80 | public void copy_exception_to_clipboard() 81 | { 82 | // ARRANGE 83 | var message = "This is the message"; 84 | var exception = new Exception(message); 85 | 86 | _applicationService.Setup(x => x.CopyToClipboard(exception.ToString())); 87 | 88 | var viewModel = new ExceptionViewModel(exception, _applicationService.Object); 89 | 90 | // ACT 91 | viewModel.CopyCommand.Execute(null); 92 | 93 | // ASSERT 94 | _applicationService.Verify(); 95 | } 96 | 97 | [Test] 98 | public void can_not_open_log_folder_when_there_is_no_log_folder() 99 | { 100 | // ARRANGE 101 | _applicationService.SetupGet(x => x.LogFolder).Returns((string)null); 102 | 103 | var viewModel = new ExceptionViewModel(null, _applicationService.Object); 104 | 105 | // ACT 106 | var canExecute = viewModel.OpenLogFolderCommand.CanExecute(null); 107 | 108 | // ASSERT 109 | Assert.That(canExecute, Is.False); 110 | } 111 | 112 | [Test] 113 | public void can_open_log_folder_when_there_is_a_log_folder() 114 | { 115 | // ARRANGE 116 | _applicationService.SetupGet(x => x.LogFolder).Returns(@"c:\temp\log.txt"); 117 | 118 | var viewModel = new ExceptionViewModel(null, _applicationService.Object); 119 | 120 | // ACT 121 | var canExecute = viewModel.OpenLogFolderCommand.CanExecute(null); 122 | 123 | // ASSERT 124 | Assert.That(canExecute, Is.True); 125 | } 126 | 127 | [Test] 128 | public void opens_log_folder() 129 | { 130 | // ARRANGE 131 | _applicationService.SetupGet(x => x.LogFolder).Returns(@"c:\temp\log.txt"); 132 | _applicationService.Setup(x => x.OpenFolder(@"c:\temp\log.txt")); 133 | 134 | var viewModel = new ExceptionViewModel(null, _applicationService.Object); 135 | 136 | // ACT 137 | viewModel.OpenLogFolderCommand.Execute(null); 138 | 139 | // ASSERT 140 | _applicationService.Verify(); 141 | } 142 | 143 | [Test] 144 | public void exit_application() 145 | { 146 | // ARRANGE 147 | _applicationService.Setup(x => x.Exit()); 148 | 149 | var viewModel = new ExceptionViewModel(null, _applicationService.Object); 150 | 151 | // ACT 152 | viewModel.ExitCommand.Execute(null); 153 | 154 | // ASSERT 155 | _applicationService.Verify(); 156 | } 157 | 158 | [Test] 159 | public void restart_application() 160 | { 161 | // ARRANGE 162 | _applicationService.Setup(x => x.Exit()); 163 | 164 | var viewModel = new ExceptionViewModel(null, _applicationService.Object); 165 | 166 | // ACT 167 | viewModel.RestartCommand.Execute(null); 168 | 169 | // ASSERT 170 | _applicationService.Verify(); 171 | } 172 | 173 | [Test] 174 | public void continue_application() 175 | { 176 | // ARRANGE 177 | _applicationService.Setup(x => x.Exit()); 178 | 179 | var viewModel = new ExceptionViewModel(null, _applicationService.Object); 180 | 181 | // ACT 182 | viewModel.ContinueCommand.Execute(null); 183 | 184 | // ASSERT 185 | _applicationService.Verify(); 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /Simple.Wpf.Exceptions/Views/Templates.xaml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 27 | 28 | 36 | 37 |