bovender framework
C# framework that implements MVVM and more
ExceptionViewModel.cs
1 /* ExceptionViewModel.cs
2  * part of Daniel's XL Toolbox NG
3  *
4  * Copyright 2014-2018 Daniel Kraus
5  *
6  * Licensed under the Apache License, Version 2.0 (the "License");
7  * you may not use this file except in compliance with the License.
8  * You may obtain a copy of the License at
9  *
10  * http://www.apache.org/licenses/LICENSE-2.0
11  *
12  * Unless required by applicable law or agreed to in writing, software
13  * distributed under the License is distributed on an "AS IS" BASIS,
14  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15  * See the License for the specific language governing permissions and
16  * limitations under the License.
17  */
18 using System;
19 using System.Collections.Generic;
20 using System.Linq;
21 using System.Text;
22 using System.Text.RegularExpressions;
23 using System.Net;
24 using System.Collections.Specialized;
25 using Microsoft.Win32;
26 using Bovender.Mvvm;
27 using Bovender.Mvvm.Messaging;
29 
30 namespace Bovender.ExceptionHandler
31 {
36  public abstract class ExceptionViewModel : ViewModelBase
37  {
38  #region Public properties
39 
40  public string User
41  {
42  get { return _user; }
43  set
44  {
45  _user = value;
46  OnPropertyChanged("User");
47  }
48  }
49 
50  public string Email
51  {
52  get { return _email; }
53  set
54  {
55  _email = value;
56  OnPropertyChanged("Email");
57  OnPropertyChanged("IsCcUserEnabled");
58  }
59  }
60 
61  public bool CcUser
62  {
63  get { return _ccUser; }
64  set
65  {
66  _ccUser = value;
67  OnPropertyChanged("CcUser");
68  }
69  }
70 
71  public bool IsCcUserEnabled
72  {
73  get
74  {
75  // TODO: Check if it is really an e-mail address
76  return !String.IsNullOrEmpty(Email);
77  }
78  }
79 
80  public string Comment
81  {
82  get
83  {
84  return _comment;
85  }
86  set
87  {
88  _comment = value;
89  OnPropertyChanged("Comment");
90  }
91  }
92 
93  public string Exception { get; private set; }
94  public string Message { get; private set; }
95  public string InnerException { get; private set; }
96  public string InnerMessage { get; private set; }
97 
98  public bool HasInnerException
99  {
100  get
101  {
102  return !String.IsNullOrEmpty(InnerException);
103  }
104  }
105 
106  public string StackTrace { get; private set; }
107 
108  public string OS
109  {
110  get
111  {
112  return Environment.OSVersion.VersionString;
113  }
114  }
115 
116  public string CLR
117  {
118  get
119  {
120  return Environment.Version.ToString();
121  }
122  }
123 
124  public string VstoRuntime
125  {
126  get
127  {
128  if (String.IsNullOrEmpty(_vstoRuntime))
129  {
130  string suffix = String.Empty;
131  RegistryKey hive = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32);
132  RegistryKey v4 = hive.OpenSubKey(@"Software\Microsoft\VSTO Runtime Setup\V4");
133  if (v4 == null)
134  {
135  v4 = hive.OpenSubKey(@"Software\Microsoft\VSTO Runtime Setup\V4R");
136  suffix = " (v4R)";
137  }
138  if (v4 != null)
139  {
140  _vstoRuntime = v4.GetValue("Version") as string;
141  }
142  if (String.IsNullOrEmpty(_vstoRuntime))
143  {
144  _vstoRuntime = "(n/a)";
145  }
146  else
147  {
148  _vstoRuntime += suffix;
149  }
150  }
151  return _vstoRuntime;
152  }
153  }
154 
155  public string ProcessBitness
156  {
157  get
158  {
159  return Environment.Is64BitProcess ? "64-bit" : "32-bit";
160  }
161  }
162 
163  public string OSBitness
164  {
165  get
166  {
167  return Environment.Is64BitOperatingSystem ? "64-bit" : "32-bit";
168  }
169  }
170 
171  public string ReportID { get; private set; }
172 
173  public string IssueUrl { get; private set; }
174 
175  public string BovenderFramework
176  {
177  get
178  {
179  return typeof(ExceptionViewModel).Assembly.GetName().Version.ToString();
180  }
181  }
182 
183  public bool IsClickOnceDeployed
184  {
185  get
186  {
187  return System.Deployment.Application.ApplicationDeployment.IsNetworkDeployed;
188  }
189  }
190 
191  #endregion
192 
193  #region Commands
194 
195  public DelegatingCommand SubmitReportCommand
196  {
197  get
198  {
199  if (_submitReportCommand == null)
200  {
201  _submitReportCommand = new DelegatingCommand(
202  (param) => DoSubmitReport(),
203  (param) => CanSubmitReport()
204  );
205  }
206  return _submitReportCommand;
207  }
208  }
209 
210  public DelegatingCommand ViewDetailsCommand
211  {
212  get
213  {
214  if (_viewDetailsCommand == null)
215  {
216  _viewDetailsCommand = new DelegatingCommand(
217  (param) => ViewDetailsMessage.Send(
218  new ViewModelMessageContent(this),
219  null)
220  );
221  }
222  return _viewDetailsCommand;
223  }
224  }
225 
226  public DelegatingCommand ClearFormCommand
227  {
228  get {
229  if (_clearFormCommand == null) {
230  _clearFormCommand = new DelegatingCommand(
231  (param) => DoClearForm(),
232  (param) => CanClearForm()
233  );
234  }
235  return _clearFormCommand;
236  }
237 
238  }
239 
240  public DelegatingCommand NavigateIssueUrlCommand
241  {
242  get
243  {
244  if (_navigateIssueUrlCommand == null)
245  {
246  _navigateIssueUrlCommand = new DelegatingCommand(
247  (param) => DoNavigateIssueUrl());
248  }
249  return _navigateIssueUrlCommand;
250  }
251  }
252  #endregion
253 
254  #region MVVM messages
255 
259  public Message<ViewModelMessageContent> ViewDetailsMessage
260  {
261  get
262  {
263  if (_viewDetailsMessage == null)
264  {
265  _viewDetailsMessage = new Message<ViewModelMessageContent>();
266  }
267  return _viewDetailsMessage;
268  }
269  }
270 
275  public Message<MessageContent> SubmitReportMessage
276  {
277  get
278  {
279  if (_submitReportMessage == null)
280  {
281  _submitReportMessage = new Message<MessageContent>();
282  }
283  return _submitReportMessage;
284  }
285  }
286 
287  #endregion
288 
289  #region Constructor
290 
295  public ExceptionViewModel(Exception e)
296  {
297  if (e != null)
298  {
299  ReportID = FileHelpers.Sha256Hash(e);
300 
301  string devPath = DevPath();
302  if (!String.IsNullOrWhiteSpace(devPath))
303  {
304  this.Exception = Regex.Replace(e.ToString(), devPath, String.Empty);
305  if (!String.IsNullOrEmpty(e.StackTrace))
306  {
307  StackTrace = Regex.Replace(e.StackTrace, devPath, String.Empty);
308  }
309  else
310  {
311  StackTrace = String.Empty;
312  }
313  }
314  Message = e.Message;
315  if (e.InnerException != null)
316  {
317  if (!String.IsNullOrWhiteSpace(devPath))
318  {
319  InnerException = Regex.Replace(e.InnerException.ToString(), devPath, String.Empty);
320  }
321  InnerMessage = e.InnerException.Message;
322  }
323  else
324  {
325  InnerException = "";
326  InnerMessage = "";
327  }
328  }
329  User = UserSettings.User;
330  Email = UserSettings.Email;
331  CcUser = UserSettings.CcUserOnExceptionReport;
332  }
333 
334  #endregion
335 
336  #region Abstract methods
337 
342  protected abstract Uri GetPostUri();
343 
344  protected abstract Bovender.UserSettings.UserSettingsBase UserSettings { get; }
345 
346  #endregion
347 
348  #region Overrides
349 
350  protected override void DoCloseView()
351  {
352  UserSettings.User = User;
353  UserSettings.Email = Email;
354  UserSettings.CcUserOnExceptionReport = CcUser;
355  base.DoCloseView();
356  }
357 
358  #endregion
359 
360  #region Private methods
361 
362  private void webClient_UploadValuesCompleted(object sender, UploadValuesCompletedEventArgs e)
363  {
364  // Set 'IsIndeterminate' to false to stop the ProgressBar animation.
365  SubmissionProcessMessageContent.IsIndeterminate = false;
366  SubmissionProcessMessageContent.WasSuccessful = false;
367  Logger.Info("Exception submission completed...");
368  if (!e.Cancelled)
369  {
370  SubmissionProcessMessageContent.WasCancelled = false;
371  if (e.Error == null)
372  {
373  string result = null;
374  try
375  {
376  result = Encoding.UTF8.GetString(e.Result);
377  // Because System.Web.Helpers is not available on every system (dispite referencing
378  // the System.Web.Helpers.dll assembly), we 'parse' the simple Json result ourselves.
379  Match m = Regex.Match(result,
380  @"{\s*""ReportId"":\s*""(?<reportid>[^""]+)"",\s*""IssueUrl"":\s*""(?<issueurl>[^""]+)""\s*}");
381  if (m.Success && m.Groups["reportid"].Value == ReportID)
382  {
383  IssueUrl = m.Groups["issueurl"].Value;
384  Logger.Info("issueUrl: {0}", IssueUrl);
385  SubmissionProcessMessageContent.WasSuccessful = true;
386  }
387  else
388  {
389  throw new UnexpectedResponseException(
390  String.Format(
391  "Received an unexpected return value from the web service (should be report ID {0}).",
392  ReportID
393  )
394  );
395  }
396  }
397  catch (Exception ex)
398  {
399  Logger.Fatal("... but response cannot be interpreted!");
400  Logger.Fatal(ex);
401  Logger.Fatal("Response: {0}", result);
402  SubmissionProcessMessageContent.Exception = new ExceptionSubmissionException(
403  "Exception submission failed", ex);
404  }
405  }
406  else
407  {
408  Logger.Warn("... with network error:");
409  Logger.Warn(e.Error);
410  SubmissionProcessMessageContent.Exception = e.Error;
411  }
412  }
413  else
414  {
415  Logger.Info("... was cancelled.");
416  SubmissionProcessMessageContent.WasCancelled = true;
417  }
418  SubmissionProcessMessageContent.Processing = false;
419  // Notify any subscribed views that the process is completed.
420  SubmissionProcessMessageContent.CompletedMessage.Send(SubmissionProcessMessageContent);
421  }
422 
423  private void CancelSubmission()
424  {
425  if (_webClient != null)
426  {
427  _webClient.CancelAsync();
428  }
429  }
430 
431  #endregion
432 
433  #region Protected methods
434 
435  protected virtual void DoSubmitReport()
436  {
437  Logger.Info("Submitting exception report");
438  SubmissionProcessMessageContent.CancelProcess = new Action(CancelSubmission);
439  SubmissionProcessMessageContent.Processing = true;
440  _webClient = new WebClient();
441  NameValueCollection v = GetPostValues();
442  _webClient.UploadValuesCompleted += webClient_UploadValuesCompleted;
443  _webClient.UploadValuesAsync(GetPostUri(), v);
444  SubmitReportMessage.Send(SubmissionProcessMessageContent);
445  }
446 
447  protected virtual bool CanSubmitReport()
448  {
449  return ((GetPostUri() != null) && !SubmissionProcessMessageContent.Processing);
450  }
451 
452  protected virtual void DoClearForm()
453  {
454  User = String.Empty;
455  Email = String.Empty;
456  Comment = String.Empty;
457  CcUser = true;
458  }
459 
460  protected virtual bool CanClearForm()
461  {
462  return !(
463  String.IsNullOrEmpty(User) &&
464  String.IsNullOrEmpty(Email) &&
465  String.IsNullOrEmpty(Comment)
466  );
467  }
468 
469  protected virtual void DoNavigateIssueUrl()
470  {
471  Logger.Info("Navigating to issue URL: {0}", IssueUrl);
472  System.Diagnostics.Process.Start(IssueUrl);
473  DoCloseView();
474  }
475 
481  protected virtual NameValueCollection GetPostValues()
482  {
483  NameValueCollection v = new NameValueCollection(20);
484  v["report_id"] = ReportID;
485  v["usersName"] = User;
486  v["usersMail"] = Email;
487  v["ccUser"] = CcUser.ToString();
488  v["exception"] = Exception;
489  v["message"] = Message;
490  v["comment"] = Comment;
491  v["inner_exception"] = InnerException;
492  v["inner_message"] = InnerMessage;
493  v["stack_trace"] = StackTrace;
494  v["process_bitness"] = ProcessBitness;
495  v["operating_system"] = OS;
496  v["os_bitness"] = OSBitness;
497  v["clr_version"] = CLR;
498  v["vstor_version"] = VstoRuntime;
499  v["bovender_version"] = BovenderFramework;
500  v["click_once"] = IsClickOnceDeployed.ToString();
501  return v;
502  }
503 
519  protected virtual string DevPath()
520  {
521  return String.Empty;
522  }
523 
524  #endregion
525 
526  #region Protected properties
527 
528  protected ProcessMessageContent SubmissionProcessMessageContent
529  {
530  get
531  {
532  if (_submissionProcessMessageContent == null)
533  {
534  _submissionProcessMessageContent = new ProcessMessageContent(
535  this,
536  new Action(CancelSubmission)
537  );
538  _submissionProcessMessageContent.IsIndeterminate = true;
539  }
540  return _submissionProcessMessageContent;
541  }
542  }
543 
544  #endregion
545 
546  #region Private fields
547 
548  private string _user;
549  private string _email;
550  private string _comment;
551  private bool _ccUser;
552  private WebClient _webClient;
553  private DelegatingCommand _submitReportCommand;
554  private DelegatingCommand _viewDetailsCommand;
555  private DelegatingCommand _clearFormCommand;
556  private DelegatingCommand _navigateIssueUrlCommand;
557  private Message<MessageContent> _submitReportMessage;
558  private Message<ViewModelMessageContent> _viewDetailsMessage;
559  private ProcessMessageContent _submissionProcessMessageContent;
560  private string _vstoRuntime;
561 
562  #endregion
563 
564  #region Class logger
565 
566  private static NLog.Logger Logger { get { return _logger.Value; } }
567 
568  private static readonly Lazy<NLog.Logger> _logger = new Lazy<NLog.Logger>(() => NLog.LogManager.GetCurrentClassLogger());
569 
570  #endregion
571  }
572 }
Holds information about percent completion of a process and defines events that occur when the proces...
Message content that holds a reference to a view model.
Command that implements ICommand and accepts delegates that contain the command implementation.
Conveys a message from a view model to a consumer (typically, a view) that has subscribed to the Sent...
Definition: Message.cs:31
Provides easy access to several system properties that are relevant for bug reports.
ExceptionViewModel(Exception e)
Instantiates the class and sets the report ID to the hexadecimal representation of the current ticks ...
Base class for persistent settings; a replacement for the UserSettings.UserSettingsBase system which ...
virtual NameValueCollection GetPostValues()
Returns a collection of key-value pairs of exception context information that will be submitted to th...
static string Sha256Hash(string file)
Computes the Sha256 hash of a given file.
Definition: FileHelpers.cs:36
virtual string DevPath()
Returns the path(s) on the development machine that shall be stripped from the file information in th...