Statistics
| Branch: | Revision:

root / trunk / Pithos.Client.WPF / Shell / ShellViewModel.cs @ c92e02f3

History | View | Annotate | Download (25.4 kB)

1
using System.Collections.Concurrent;
2
using System.ComponentModel;
3
using System.ComponentModel.Composition;
4
using System.Diagnostics;
5
using System.Diagnostics.Contracts;
6
using System.IO;
7
using System.Net;
8
using System.Reflection;
9
using System.Runtime.InteropServices;
10
using System.ServiceModel;
11
using System.ServiceModel.Description;
12
using System.Threading.Tasks;
13
using System.Windows;
14
using Caliburn.Micro;
15
using Hardcodet.Wpf.TaskbarNotification;
16
using Pithos.Client.WPF.Configuration;
17
using Pithos.Client.WPF.FileProperties;
18
using Pithos.Client.WPF.Properties;
19
using Pithos.Client.WPF.SelectiveSynch;
20
using Pithos.Client.WPF.Services;
21
using Pithos.Client.WPF.Shell;
22
using Pithos.Core;
23
using Pithos.Interfaces;
24
using System;
25
using System.Collections.Generic;
26
using System.Linq;
27
using System.Text;
28
using Pithos.Network;
29
using StatusService = Pithos.Client.WPF.Services.StatusService;
30

    
31
namespace Pithos.Client.WPF {
32
    using System.ComponentModel.Composition;
33

    
34
    [Export(typeof(IShell))]
35
	///<summary>
36
	/// The "shell" of the Pithos application displays the taskbar  icon, menu and notifications.
37
	/// The shell also hosts the status service called by shell extensions to retrieve file info
38
	///</summary>
39
	///<remarks>
40
	/// It is a strange "shell" as its main visible element is an icon instead of a window
41
	/// The shell subscribes to the following events:
42
	/// * Notification:  Raised by components that want to notify the user. Usually displayed in a balloon
43
	/// * SelectiveSynchChanges: Notifies that the user made changes to the selective synch folders for an account. Raised by the Selective Synch dialog. Located here because the monitors are here
44
	/// * ShowFilePropertiesEvent: Raised when a shell command requests the display of the file/container properties dialog
45
	///</remarks>		
46
	//TODO: CODE SMELL Why does the shell handle the SelectiveSynchChanges?
47
    public class ShellViewModel : Screen, IStatusNotification, IShell,
48
        IHandle<Notification>, IHandle<SelectiveSynchChanges>, IHandle<ShowFilePropertiesEvent>
49
    {
50
		//The Status Checker provides the current synch state
51
		//TODO: Could we remove the status checker and use events in its place?
52
        private IStatusChecker _statusChecker;
53
        private IEventAggregator _events;
54

    
55
        public PithosSettings Settings { get; private set; }        
56

    
57
		
58
        private Dictionary<string, PithosMonitor> _monitors = new Dictionary<string, PithosMonitor>();
59
		///<summary>
60
		/// Dictionary of account monitors, keyed by account
61
		///</summary>
62
		///<remarks>
63
		/// One monitor class is created for each account. The Shell needs access to the monitors to execute start/stop/pause commands,
64
		/// retrieve account and boject info		
65
		///</remarks>
66
		// TODO: Does the Shell REALLY need access to the monitors? Could we achieve the same results with a better design?
67
		// TODO: The monitors should be internal to Pithos.Core, even though exposing them makes coding of the Object and Container windows easier
68
        public Dictionary<string, PithosMonitor> Monitors
69
        {
70
            get { return _monitors; }
71
        }
72

    
73

    
74
		///<summary>
75
		/// The status service is used by Shell extensions to retrieve file status information
76
		///</summary>
77
		//TODO: CODE SMELL! This is the shell! While hosting in the shell makes executing start/stop commands easier, it is still a smell
78
        private ServiceHost _statusService { get; set; }
79

    
80
		//Logging in the Pithos client is provided by log4net
81
        private static readonly log4net.ILog Log = log4net.LogManager.GetLogger("Pithos");
82

    
83
		///<summary>
84
		/// The Shell depends on MEF to provide implementations for windowManager, events, the status checker service and the settings
85
		///</summary>
86
		///<remarks>
87
		/// The PithosSettings class encapsulates the app's settings to abstract their storage mechanism (App settings, a database or registry)
88
		///</remarks>
89
        [ImportingConstructor]		
90
        public ShellViewModel(IWindowManager windowManager, IEventAggregator events, IStatusChecker statusChecker, PithosSettings settings)
91
        {
92
            try
93
            {
94

    
95
                _windowManager = windowManager;
96
				//CHECK: Caliburn doesn't need explicit command construction
97
                //OpenPithosFolderCommand = new PithosCommand(OpenPithosFolder);
98
                _statusChecker = statusChecker;
99
				//The event subst
100
                _events = events;
101
                _events.Subscribe(this);
102

    
103
                Settings = settings;
104

    
105
                StatusMessage = "In Synch";
106

    
107
            }
108
            catch (Exception exc)
109
            {
110
                Log.Error("Error while starting the ShellViewModel",exc);
111
                throw;
112
            }
113
        }
114

    
115
        protected override void OnActivate()
116
        {
117
            base.OnActivate();
118

    
119
            StartMonitoring();                    
120
        }
121

    
122

    
123
        private async Task StartMonitoring()
124
        {
125
            try
126
            {                
127
                foreach (var account in Settings.Accounts)
128
                {
129
                    await MonitorAccount(account);
130
                }
131
                _statusService = StatusService.Start();
132
            }
133
            catch (AggregateException exc)
134
            {
135
                exc.Handle(e =>
136
                {
137
                    Log.Error("Error while starting monitoring", e);
138
                    return true;
139
                });
140
                throw;
141
            }
142
        }
143

    
144
        protected override void OnDeactivate(bool close)
145
        {
146
            base.OnDeactivate(close);
147
            if (close)
148
            {
149
                StatusService.Stop(_statusService);
150
                _statusService = null;
151
            }
152
        }
153

    
154
        public Task MonitorAccount(AccountSettings account)
155
        {
156
            return Task.Factory.StartNew(() =>
157
            {                                                
158
                PithosMonitor monitor = null;
159
                var accountName = account.AccountName;
160

    
161
                if (_monitors.TryGetValue(accountName, out monitor))
162
                {
163
                    //If the account is active
164
                    if (account.IsActive)
165
                        //Start the monitor. It's OK to start an already started monitor,
166
                        //it will just ignore the call
167
                        monitor.Start();
168
                    else
169
                    {
170
                        //If the account is inactive
171
                        //Stop and remove the monitor
172
                        RemoveMonitor(accountName);
173
                    }
174
                    return;
175
                }
176

    
177
                //Create a new monitor/ Can't use MEF here, it would return a single instance for all monitors
178
                monitor = new PithosMonitor
179
                              {
180
                                  UserName = accountName,
181
                                  ApiKey = account.ApiKey,
182
                                  UsePithos = account.UsePithos,
183
                                  StatusNotification = this,
184
                                  RootPath = account.RootPath
185
                              };
186
                //PithosMonitor uses MEF so we need to resolve it
187
                IoC.BuildUp(monitor);
188

    
189
                var appSettings = Properties.Settings.Default;
190
                monitor.AuthenticationUrl = account.UsePithos
191
                                                ? appSettings.PithosAuthenticationUrl
192
                                                : appSettings.CloudfilesAuthenticationUrl;
193

    
194
                _monitors[accountName] = monitor;
195

    
196
                if (account.IsActive)
197
                {
198
                    //Don't start a monitor if it doesn't have an account and ApiKey
199
                    if (String.IsNullOrWhiteSpace(monitor.UserName) ||
200
                        String.IsNullOrWhiteSpace(monitor.ApiKey))
201
                        return;
202
                    StartMonitor(monitor);
203
                }
204
            });
205
        }
206

    
207

    
208
        protected override void OnViewLoaded(object view)
209
        {
210
            var window = (Window)view;
211
            window.Hide();
212
            UpdateStatus();
213
            base.OnViewLoaded(view);
214
        }
215

    
216

    
217
        #region Status Properties
218

    
219
        private string _statusMessage;
220
        public string StatusMessage
221
        {
222
            get { return _statusMessage; }
223
            set
224
            {
225
                _statusMessage = value;
226
                NotifyOfPropertyChange(() => StatusMessage);
227
            }
228
        }
229

    
230
        private ObservableConcurrentCollection<AccountInfo> _accounts = new ObservableConcurrentCollection<AccountInfo>();
231
        public ObservableConcurrentCollection<AccountInfo> Accounts
232
        {
233
            get { return _accounts; }
234
        }
235

    
236

    
237
        private string _pauseSyncCaption="Pause Syncing";
238
        public string PauseSyncCaption
239
        {
240
            get { return _pauseSyncCaption; }
241
            set
242
            {
243
                _pauseSyncCaption = value;
244
                NotifyOfPropertyChange(() => PauseSyncCaption);
245
            }
246
        }
247

    
248
        private readonly ObservableConcurrentCollection<FileEntry> _recentFiles = new ObservableConcurrentCollection<FileEntry>();
249
        public ObservableConcurrentCollection<FileEntry> RecentFiles
250
        {
251
            get { return _recentFiles; }
252
        }
253

    
254

    
255
        private string _statusIcon="../Images/Tray.ico";
256
        public string StatusIcon
257
        {
258
            get { return _statusIcon; }
259
            set
260
            {
261
                _statusIcon = value;
262
                NotifyOfPropertyChange(() => StatusIcon);
263
            }
264
        }
265

    
266
        #endregion
267

    
268
        #region Commands
269

    
270
        public void ShowPreferences()
271
        {
272
            Settings.Reload();
273
            var preferences = new PreferencesViewModel(_windowManager,_events, this,Settings);            
274
            _windowManager.ShowDialog(preferences);
275
            
276
        }
277

    
278
        public void AboutPithos()
279
        {
280
            var about = new AboutViewModel();
281
            _windowManager.ShowWindow(about);
282
        }
283

    
284
        public void SendFeedback()
285
        {
286
            var feedBack =  IoC.Get<FeedbackViewModel>();
287
            _windowManager.ShowWindow(feedBack);
288
        }
289

    
290
        //public PithosCommand OpenPithosFolderCommand { get; private set; }
291

    
292
        public void OpenPithosFolder()
293
        {
294
            var account = Settings.Accounts.FirstOrDefault(acc => acc.IsActive);
295
            if (account == null)
296
                return;
297
            Process.Start(account.RootPath);
298
        }
299

    
300
        public void OpenPithosFolder(AccountInfo account)
301
        {
302
            Process.Start(account.AccountPath);
303
        }
304

    
305
        
306
        public void GoToSite(AccountInfo account)
307
        {
308
            var site = String.Format("{0}/ui/?token={1}&user={2}",
309
                Properties.Settings.Default.PithosSite,account.Token,
310
                account.UserName);
311
            Process.Start(site);
312
        }
313

    
314
        public void ShowFileProperties()
315
        {
316
            var account = Settings.Accounts.First(acc => acc.IsActive);            
317
            var dir = new DirectoryInfo(account.RootPath + @"\pithos");
318
            var files=dir.GetFiles();
319
            var r=new Random();
320
            var idx=r.Next(0, files.Length);
321
            ShowFileProperties(files[idx].FullName);            
322
        }
323

    
324
        public void ShowFileProperties(string filePath)
325
        {
326
            if (String.IsNullOrWhiteSpace(filePath))
327
                throw new ArgumentNullException("filePath");
328
            if (!File.Exists(filePath))
329
                throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");
330
            Contract.EndContractBlock();
331

    
332
            var pair=(from monitor in  Monitors
333
                               where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
334
                                   select monitor).FirstOrDefault();
335
            var account = pair.Key;
336
            var accountMonitor = pair.Value;
337

    
338
            if (accountMonitor == null)
339
                return;
340

    
341
            var infoTask=Task.Factory.StartNew(()=>accountMonitor.GetObjectInfo(filePath));
342

    
343
            
344

    
345
            var fileProperties = new FilePropertiesViewModel(this, infoTask,filePath);
346
            _windowManager.ShowWindow(fileProperties);
347
        } 
348
        
349
        public void ShowContainerProperties()
350
        {
351
            var account = Settings.Accounts.First(acc => acc.IsActive);            
352
            var dir = new DirectoryInfo(account.RootPath);
353
            var fullName = (from folder in dir.EnumerateDirectories()
354
                            where (folder.Attributes & FileAttributes.Hidden) == 0
355
                            select folder.FullName).First();
356
            ShowContainerProperties(fullName);            
357
        }
358

    
359
        public void ShowContainerProperties(string filePath)
360
        {
361
            if (String.IsNullOrWhiteSpace(filePath))
362
                throw new ArgumentNullException("filePath");
363
            if (!Directory.Exists(filePath))
364
                throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");
365
            Contract.EndContractBlock();
366

    
367
            var pair=(from monitor in  Monitors
368
                               where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
369
                                   select monitor).FirstOrDefault();
370
            var account = pair.Key;
371
            var accountMonitor = pair.Value;            
372
            var info = accountMonitor.GetContainerInfo(filePath);
373

    
374
            
375

    
376
            var containerProperties = new ContainerPropertiesViewModel(this, info,filePath);
377
            _windowManager.ShowWindow(containerProperties);
378
        }
379

    
380
        public ObjectInfo RefreshObjectInfo(ObjectInfo currentInfo)
381
        {
382
            if (currentInfo==null)
383
                throw new ArgumentNullException("currentInfo");
384
            Contract.EndContractBlock();
385

    
386
            var monitor = Monitors[currentInfo.Account];
387
            var newInfo=monitor.CloudClient.GetObjectInfo(currentInfo.Account, currentInfo.Container, currentInfo.Name);
388
            return newInfo;
389
        }
390

    
391
        public ContainerInfo RefreshContainerInfo(ContainerInfo container)
392
        {
393
            if (container == null)
394
                throw new ArgumentNullException("container");
395
            Contract.EndContractBlock();
396

    
397
            var monitor = Monitors[container.Account];
398
            var newInfo = monitor.CloudClient.GetContainerInfo(container.Account, container.Name);
399
            return newInfo;
400
        }
401

    
402

    
403
        public void ToggleSynching()
404
        {
405
            bool isPaused=false;
406
            foreach (var pair in Monitors)
407
            {
408
                var monitor = pair.Value;
409
                monitor.Pause = !monitor.Pause;
410
                isPaused = monitor.Pause;
411
            }
412

    
413
            PauseSyncCaption = isPaused ? "Resume syncing" : "Pause syncing";
414
            var iconKey = isPaused? "TraySyncPaused" : "TrayInSynch";
415
            StatusIcon = String.Format(@"../Images/{0}.ico", iconKey);
416
        }
417

    
418
        public void ExitPithos()
419
        {
420
            foreach (var pair in Monitors)
421
            {
422
                var monitor = pair.Value;
423
                monitor.Stop();
424
            }
425

    
426
            ((Window)GetView()).Close();
427
        }
428
        #endregion
429

    
430

    
431
        private Dictionary<PithosStatus, StatusInfo> iconNames = new List<StatusInfo>
432
            {
433
                new StatusInfo(PithosStatus.InSynch, "All files up to date", "TrayInSynch"),
434
                new StatusInfo(PithosStatus.Syncing, "Syncing Files", "TraySynching"),
435
                new StatusInfo(PithosStatus.SyncPaused, "Sync Paused", "TraySyncPaused")
436
            }.ToDictionary(s => s.Status);
437

    
438
        readonly IWindowManager _windowManager;
439

    
440

    
441
		///<summary>
442
		/// Updates the visual status indicators of the application depending on status changes, e.g. icon, stat		
443
		///</summary>
444
        public void UpdateStatus()
445
        {
446
            var pithosStatus = _statusChecker.GetPithosStatus();
447

    
448
            if (iconNames.ContainsKey(pithosStatus))
449
            {
450
                var info = iconNames[pithosStatus];
451
                StatusIcon = String.Format(@"../Images/{0}.ico", info.IconName);
452

    
453
                Assembly assembly = Assembly.GetExecutingAssembly();                               
454
                var fileVersion = FileVersionInfo.GetVersionInfo(assembly.Location);
455

    
456

    
457
                StatusMessage = String.Format("Pithos {0}\r\n{1}", fileVersion.FileVersion,info.StatusText);
458
            }
459
            
460
            _events.Publish(new Notification { Title = "Start", Message = "Start Monitoring", Level = TraceLevel.Info});
461
        }
462

    
463

    
464
       
465
        private Task StartMonitor(PithosMonitor monitor,int retries=0)
466
        {
467
            return Task.Factory.StartNew(() =>
468
            {
469
                using (log4net.ThreadContext.Stacks["Monitor"].Push("Start"))
470
                {
471
                    try
472
                    {
473
                        Log.InfoFormat("Start Monitoring {0}", monitor.UserName);
474

    
475
                        monitor.Start();
476
                    }
477
                    catch (WebException exc)
478
                    {
479
                        if (AbandonRetry(monitor, retries))
480
                            return;
481

    
482
                        if (IsUnauthorized(exc))
483
                        {
484
                            var message = String.Format("API Key Expired for {0}. Starting Renewal",monitor.UserName);                            
485
                            Log.Error(message,exc);
486
                            TryAuthorize(monitor,retries).Wait();
487
                        }
488
                        else
489
                        {
490
                            TryLater(monitor, exc,retries);
491
                        }
492
                    }
493
                    catch (Exception exc)
494
                    {
495
                        if (AbandonRetry(monitor, retries)) 
496
                            return;
497

    
498
                        TryLater(monitor,exc,retries);
499
                    }
500
                }
501
            });
502
        }
503

    
504
        private bool AbandonRetry(PithosMonitor monitor, int retries)
505
        {
506
            if (retries > 1)
507
            {
508
                var message = String.Format("Monitoring of account {0} has failed too many times. Will not retry",
509
                                            monitor.UserName);
510
                _events.Publish(new Notification
511
                                    {Title = "Account monitoring failed", Message = message, Level = TraceLevel.Error});
512
                return true;
513
            }
514
            return false;
515
        }
516

    
517

    
518
        private async Task TryAuthorize(PithosMonitor monitor,int retries)
519
        {
520
            _events.Publish(new Notification { Title = "Authorization failed", Message = "Your API Key has probably expired. You will be directed to a page where you can renew it", Level = TraceLevel.Error });
521

    
522
            try
523
            {
524

    
525
                var credentials = await PithosAccount.RetrieveCredentialsAsync(Settings.PithosLoginUrl);
526

    
527
                var account = Settings.Accounts.FirstOrDefault(act => act.AccountName == credentials.UserName);
528
                account.ApiKey = credentials.Password;
529
                monitor.ApiKey = credentials.Password;
530
                Settings.Save();
531
                await TaskEx.Delay(10000);
532
                StartMonitor(monitor, retries + 1);
533
                NotifyOfPropertyChange(()=>Accounts);
534
            }
535
            catch (AggregateException exc)
536
            {
537
                string message = String.Format("API Key retrieval for {0} failed", monitor.UserName);
538
                Log.Error(message, exc.InnerException);
539
                _events.Publish(new Notification { Title = "Authorization failed", Message = message, Level = TraceLevel.Error });
540
                return;
541
            }
542
            catch (Exception exc)
543
            {
544
                string message = String.Format("API Key retrieval for {0} failed", monitor.UserName);
545
                Log.Error(message, exc);
546
                _events.Publish(new Notification { Title = "Authorization failed", Message = message, Level = TraceLevel.Error });
547
                return;
548
                
549
            }
550

    
551
        }
552

    
553
        private static bool IsUnauthorized(WebException exc)
554
        {
555
            if (exc==null)
556
                throw new ArgumentNullException("exc");
557
            Contract.EndContractBlock();
558

    
559
            var response = exc.Response as HttpWebResponse;
560
            if (response == null)
561
                return false;
562
            return (response.StatusCode == HttpStatusCode.Unauthorized);
563
        }
564

    
565
        private void TryLater(PithosMonitor monitor, Exception exc,int retries)
566
        {
567
            var message = String.Format("An exception occured. Can't start monitoring\nWill retry in 10 seconds");
568
            Task.Factory.StartNewDelayed(10000, () => StartMonitor(monitor,retries+1));
569
            _events.Publish(new Notification
570
                                {Title = "Error", Message = message, Level = TraceLevel.Error});
571
            Log.Error(message, exc);
572
        }
573

    
574

    
575
        public void NotifyChange(string status, TraceLevel level=TraceLevel.Info)
576
        {
577
            this.StatusMessage = status;
578
            
579
            _events.Publish(new Notification { Title = "Pithos", Message = status, Level = level });
580
        }
581

    
582
        public void NotifyChangedFile(string filePath)
583
        {
584
            var entry = new FileEntry {FullPath=filePath};
585
            IProducerConsumerCollection<FileEntry> files=this.RecentFiles;
586
            FileEntry popped;
587
            while (files.Count > 5)
588
                files.TryTake(out popped);
589
            files.TryAdd(entry);
590
        }
591

    
592
        public void NotifyAccount(AccountInfo account)
593
        {
594
            if (account== null)
595
                return;
596

    
597
            account.SiteUri= String.Format("{0}/ui/?token={1}&user={2}",
598
                Properties.Settings.Default.PithosSite, account.Token,
599
                account.UserName);
600

    
601
            IProducerConsumerCollection<AccountInfo> accounts = Accounts;
602
            for (var i = 0; i < _accounts.Count; i++)
603
            {
604
                AccountInfo item;
605
                if (accounts.TryTake(out item))
606
                {
607
                    if (item.UserName!=account.UserName)
608
                    {
609
                        accounts.TryAdd(item);
610
                    }
611
                }
612
            }
613

    
614
            accounts.TryAdd(account);
615
        }
616

    
617

    
618
        public void RemoveMonitor(string accountName)
619
        {
620
            if (String.IsNullOrWhiteSpace(accountName))
621
                return;
622

    
623
            PithosMonitor monitor;
624
            if (Monitors.TryGetValue(accountName, out monitor))
625
            {
626
                Monitors.Remove(accountName);
627
                monitor.Stop();
628
            }
629
        }
630

    
631
        public void RefreshOverlays()
632
        {
633
            foreach (var pair in Monitors)
634
            {
635
                var monitor = pair.Value;
636

    
637
                var path = monitor.RootPath;
638

    
639
                if (String.IsNullOrWhiteSpace(path))
640
                    continue;
641

    
642
                if (!Directory.Exists(path) && !File.Exists(path))
643
                    continue;
644

    
645
                IntPtr pathPointer = Marshal.StringToCoTaskMemAuto(path);
646

    
647
                try
648
                {
649
                    NativeMethods.SHChangeNotify(HChangeNotifyEventID.SHCNE_UPDATEITEM,
650
                                                 HChangeNotifyFlags.SHCNF_PATHW | HChangeNotifyFlags.SHCNF_FLUSHNOWAIT,
651
                                                 pathPointer, IntPtr.Zero);
652
                }
653
                finally
654
                {
655
                    Marshal.FreeHGlobal(pathPointer);
656
                }
657
            }
658
        }
659

    
660
        #region Event Handlers
661
        
662
        public void Handle(SelectiveSynchChanges message)
663
        {
664
            var accountName = message.Account.AccountName;
665
            PithosMonitor monitor;
666
            if (_monitors.TryGetValue(accountName, out monitor))
667
            {
668
                monitor.AddSelectivePaths(message.Added);
669
                monitor.RemoveSelectivePaths(message.Removed);
670

    
671
            }
672
            
673
        }
674

    
675

    
676
        public void Handle(Notification notification)
677
        {
678
            if (!Settings.ShowDesktopNotifications)
679
                return;
680
            BalloonIcon icon = BalloonIcon.None;
681
            switch (notification.Level)
682
            {
683
                case TraceLevel.Error:
684
                    icon = BalloonIcon.Error;
685
                    break;
686
                case TraceLevel.Info:
687
                case TraceLevel.Verbose:
688
                    icon = BalloonIcon.Info;
689
                    break;
690
                case TraceLevel.Warning:
691
                    icon = BalloonIcon.Warning;
692
                    break;
693
                default:
694
                    icon = BalloonIcon.None;
695
                    break;
696
            }
697

    
698
            if (Settings.ShowDesktopNotifications)
699
            {
700
                var tv = (ShellView) this.GetView();
701
                tv.TaskbarView.ShowBalloonTip(notification.Title, notification.Message, icon);
702
            }
703
        }
704
        #endregion
705

    
706
        public void Handle(ShowFilePropertiesEvent message)
707
        {
708
            if (message == null)
709
                throw new ArgumentNullException("message");
710
            if (String.IsNullOrWhiteSpace(message.FileName) )
711
                throw new ArgumentException("message");
712
            Contract.EndContractBlock();
713

    
714
            var fileName = message.FileName;
715

    
716
            if (File.Exists(fileName))
717
                this.ShowFileProperties(fileName);
718
            else if (Directory.Exists(fileName))
719
                this.ShowContainerProperties(fileName);
720
        }
721
    }
722
}