Changed proxy settings to request update if the proxy requires authentication
[pithos-ms-client] / trunk / Pithos.Client.WPF / Shell / ShellViewModel.cs
1 using System.Collections.Concurrent;
2 using System.Diagnostics;
3 using System.Diagnostics.Contracts;
4 using System.IO;
5 using System.Net;
6 using System.Reflection;
7 using System.Runtime.InteropServices;
8 using System.ServiceModel;
9 using System.Threading.Tasks;
10 using System.Windows;
11 using Caliburn.Micro;
12 using Hardcodet.Wpf.TaskbarNotification;
13 using Pithos.Client.WPF.Configuration;
14 using Pithos.Client.WPF.FileProperties;
15 using Pithos.Client.WPF.Preferences;
16 using Pithos.Client.WPF.SelectiveSynch;
17 using Pithos.Client.WPF.Services;
18 using Pithos.Client.WPF.Shell;
19 using Pithos.Core;
20 using Pithos.Interfaces;
21 using System;
22 using System.Collections.Generic;
23 using System.Linq;
24 using Pithos.Network;
25 using StatusService = Pithos.Client.WPF.Services.StatusService;
26
27 namespace Pithos.Client.WPF {
28         using System.ComponentModel.Composition;
29
30         
31         ///<summary>
32         /// The "shell" of the Pithos application displays the taskbar  icon, menu and notifications.
33         /// The shell also hosts the status service called by shell extensions to retrieve file info
34         ///</summary>
35         ///<remarks>
36         /// It is a strange "shell" as its main visible element is an icon instead of a window
37         /// The shell subscribes to the following events:
38         /// * Notification:  Raised by components that want to notify the user. Usually displayed in a balloon
39         /// * 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
40         /// * ShowFilePropertiesEvent: Raised when a shell command requests the display of the file/container properties dialog
41         ///</remarks>           
42         //TODO: CODE SMELL Why does the shell handle the SelectiveSynchChanges?
43         [Export(typeof(IShell))]
44         public class ShellViewModel : Screen, IStatusNotification, IShell,
45                 IHandle<Notification>, IHandle<SelectiveSynchChanges>, IHandle<ShowFilePropertiesEvent>
46         {
47                 //The Status Checker provides the current synch state
48                 //TODO: Could we remove the status checker and use events in its place?
49                 private readonly IStatusChecker _statusChecker;
50                 private readonly IEventAggregator _events;
51
52                 public PithosSettings Settings { get; private set; }
53
54
55         private readonly ConcurrentDictionary<string, PithosMonitor> _monitors = new ConcurrentDictionary<string, PithosMonitor>();
56                 ///<summary>
57                 /// Dictionary of account monitors, keyed by account
58                 ///</summary>
59                 ///<remarks>
60                 /// One monitor class is created for each account. The Shell needs access to the monitors to execute start/stop/pause commands,
61                 /// retrieve account and boject info            
62                 ///</remarks>
63                 // TODO: Does the Shell REALLY need access to the monitors? Could we achieve the same results with a better design?
64                 // TODO: The monitors should be internal to Pithos.Core, even though exposing them makes coding of the Object and Container windows easier
65         public ConcurrentDictionary<string, PithosMonitor> Monitors
66                 {
67                         get { return _monitors; }
68                 }
69
70
71             ///<summary>
72             /// The status service is used by Shell extensions to retrieve file status information
73             ///</summary>
74             //TODO: CODE SMELL! This is the shell! While hosting in the shell makes executing start/stop commands easier, it is still a smell
75             private ServiceHost _statusService;
76
77                 //Logging in the Pithos client is provided by log4net
78                 private static readonly log4net.ILog Log = log4net.LogManager.GetLogger("Pithos");
79
80                 ///<summary>
81                 /// The Shell depends on MEF to provide implementations for windowManager, events, the status checker service and the settings
82                 ///</summary>
83                 ///<remarks>
84                 /// The PithosSettings class encapsulates the app's settings to abstract their storage mechanism (App settings, a database or registry)
85                 ///</remarks>
86                 [ImportingConstructor]          
87                 public ShellViewModel(IWindowManager windowManager, IEventAggregator events, IStatusChecker statusChecker, PithosSettings settings)
88                 {
89                         try
90                         {
91
92                                 _windowManager = windowManager;
93                                 //CHECK: Caliburn doesn't need explicit command construction
94                                 //OpenPithosFolderCommand = new PithosCommand(OpenPithosFolder);
95                                 _statusChecker = statusChecker;
96                                 //The event subst
97                                 _events = events;
98                                 _events.Subscribe(this);
99
100                                 Settings = settings;
101
102                                 StatusMessage = "In Synch";
103
104                                 _accounts.CollectionChanged += (sender, e) =>
105                                                                                                    {
106                                                                                                            NotifyOfPropertyChange(() => OpenFolderCaption);
107                                                                                                            NotifyOfPropertyChange(() => HasAccounts);
108                                                                                                    };
109
110                         }
111                         catch (Exception exc)
112                         {
113                                 Log.Error("Error while starting the ShellViewModel",exc);
114                                 throw;
115                         }
116                 }
117
118
119                 protected override void OnActivate()
120                 {
121                         base.OnActivate();
122
123                         StartMonitoring();                    
124                 }
125
126
127                 private async void StartMonitoring()
128                 {
129                         try
130                         {
131                                 var accounts = Settings.Accounts.Select(MonitorAccount);
132                                 await TaskEx.WhenAll(accounts);
133                                 _statusService = StatusService.Start();
134
135 /*
136                                 foreach (var account in Settings.Accounts)
137                                 {
138                                         await MonitorAccount(account);
139                                 }
140 */
141                                 
142                         }
143                         catch (AggregateException exc)
144                         {
145                                 exc.Handle(e =>
146                                 {
147                                         Log.Error("Error while starting monitoring", e);
148                                         return true;
149                                 });
150                                 throw;
151                         }
152                 }
153
154                 protected override void OnDeactivate(bool close)
155                 {
156                         base.OnDeactivate(close);
157                         if (close)
158                         {
159                                 StatusService.Stop(_statusService);
160                                 _statusService = null;
161                         }
162                 }
163
164                 public Task MonitorAccount(AccountSettings account)
165                 {
166                         return Task.Factory.StartNew(() =>
167                         {                                                
168                                 PithosMonitor monitor;
169                                 var accountName = account.AccountName;
170
171                                 if (_monitors.TryGetValue(accountName, out monitor))
172                                 {
173                                         //If the account is active
174                                         if (account.IsActive)
175                                                 //Start the monitor. It's OK to start an already started monitor,
176                                                 //it will just ignore the call                        
177                                                 StartMonitor(monitor).Wait();                        
178                                         else
179                                         {
180                                                 //If the account is inactive
181                                                 //Stop and remove the monitor
182                                                 RemoveMonitor(accountName);
183                                         }
184                                         return;
185                                 }
186
187                                 //Create a new monitor/ Can't use MEF here, it would return a single instance for all monitors
188                                 monitor = new PithosMonitor
189                                                           {
190                                                                   UserName = accountName,
191                                                                   ApiKey = account.ApiKey,                                  
192                                                                   StatusNotification = this,
193                                                                   RootPath = account.RootPath
194                                                           };
195                                 //PithosMonitor uses MEF so we need to resolve it
196                                 IoC.BuildUp(monitor);
197
198                             monitor.AuthenticationUrl = account.ServerUrl;
199
200                                 _monitors[accountName] = monitor;
201
202                                 if (account.IsActive)
203                                 {
204                                         //Don't start a monitor if it doesn't have an account and ApiKey
205                                         if (String.IsNullOrWhiteSpace(monitor.UserName) ||
206                                                 String.IsNullOrWhiteSpace(monitor.ApiKey))
207                                                 return;
208                                         StartMonitor(monitor);
209                                 }
210                         });
211                 }
212
213
214                 protected override void OnViewLoaded(object view)
215                 {
216                         UpdateStatus();
217                         var window = (Window)view;            
218                         TaskEx.Delay(1000).ContinueWith(t => Execute.OnUIThread(window.Hide));
219                         base.OnViewLoaded(view);
220                 }
221
222
223                 #region Status Properties
224
225                 private string _statusMessage;
226                 public string StatusMessage
227                 {
228                         get { return _statusMessage; }
229                         set
230                         {
231                                 _statusMessage = value;
232                                 NotifyOfPropertyChange(() => StatusMessage);
233                         }
234                 }
235
236                 private readonly ObservableConcurrentCollection<AccountInfo> _accounts = new ObservableConcurrentCollection<AccountInfo>();
237                 public ObservableConcurrentCollection<AccountInfo> Accounts
238                 {
239                         get { return _accounts; }
240                 }
241
242                 public bool HasAccounts
243                 {
244                         get { return _accounts.Count > 0; }
245                 }
246
247
248                 public string OpenFolderCaption
249                 {
250                         get
251                         {
252                                 return (_accounts.Count == 0)
253                                                 ? "No Accounts Defined"
254                                                 : "Open Pithos Folder";
255                         }
256                 }
257
258                 private string _pauseSyncCaption="Pause Synching";
259                 public string PauseSyncCaption
260                 {
261                         get { return _pauseSyncCaption; }
262                         set
263                         {
264                                 _pauseSyncCaption = value;
265                                 NotifyOfPropertyChange(() => PauseSyncCaption);
266                         }
267                 }
268
269                 private readonly ObservableConcurrentCollection<FileEntry> _recentFiles = new ObservableConcurrentCollection<FileEntry>();
270                 public ObservableConcurrentCollection<FileEntry> RecentFiles
271                 {
272                         get { return _recentFiles; }
273                 }
274
275
276                 private string _statusIcon="../Images/Pithos.ico";
277                 public string StatusIcon
278                 {
279                         get { return _statusIcon; }
280                         set
281                         {
282                 //TODO: Ensure all status icons use the Pithos logo
283                                 _statusIcon = value;
284                                 NotifyOfPropertyChange(() => StatusIcon);
285                         }
286                 }
287
288                 #endregion
289
290                 #region Commands
291
292                 public void ShowPreferences()
293                 {
294                         Settings.Reload();
295                         var preferences = new PreferencesViewModel(_windowManager,_events, this,Settings);            
296                         _windowManager.ShowDialog(preferences);
297                         
298                 }
299
300                 public void AboutPithos()
301                 {
302                         var about = new AboutViewModel();
303                         _windowManager.ShowWindow(about);
304                 }
305
306                 public void SendFeedback()
307                 {
308                         var feedBack =  IoC.Get<FeedbackViewModel>();
309                         _windowManager.ShowWindow(feedBack);
310                 }
311
312                 //public PithosCommand OpenPithosFolderCommand { get; private set; }
313
314                 public void OpenPithosFolder()
315                 {
316                         var account = Settings.Accounts.FirstOrDefault(acc => acc.IsActive);
317                         if (account == null)
318                                 return;
319                         Process.Start(account.RootPath);
320                 }
321
322                 public void OpenPithosFolder(AccountInfo account)
323                 {
324                         Process.Start(account.AccountPath);
325                 }
326
327                 
328 /*
329                 public void GoToSite()
330                 {            
331                         var site = Properties.Settings.Default.PithosSite;
332                         Process.Start(site);            
333                 }
334 */
335
336                 public void GoToSite(AccountInfo account)
337                 {
338                         /*var site = String.Format("{0}/ui/?token={1}&user={2}",
339                                 account.SiteUri,account.Token,
340                                 account.UserName);*/
341                         Process.Start(account.SiteUri);
342                 }
343
344                 public void ShowFileProperties()
345                 {
346                         var account = Settings.Accounts.First(acc => acc.IsActive);            
347                         var dir = new DirectoryInfo(account.RootPath + @"\pithos");
348                         var files=dir.GetFiles();
349                         var r=new Random();
350                         var idx=r.Next(0, files.Length);
351                         ShowFileProperties(files[idx].FullName);            
352                 }
353
354                 public void ShowFileProperties(string filePath)
355                 {
356                         if (String.IsNullOrWhiteSpace(filePath))
357                                 throw new ArgumentNullException("filePath");
358                         if (!File.Exists(filePath) && !Directory.Exists(filePath))
359                                 throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");
360                         Contract.EndContractBlock();
361
362                         var pair=(from monitor in  Monitors
363                                                            where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
364                                                                    select monitor).FirstOrDefault();
365                     var accountMonitor = pair.Value;
366
367                         if (accountMonitor == null)
368                                 return;
369
370                         var infoTask=Task.Factory.StartNew(()=>accountMonitor.GetObjectInfo(filePath));
371
372                         
373
374                         var fileProperties = new FilePropertiesViewModel(this, infoTask,filePath);
375                         _windowManager.ShowWindow(fileProperties);
376                 } 
377                 
378                 public void ShowContainerProperties()
379                 {
380                         var account = Settings.Accounts.First(acc => acc.IsActive);            
381                         var dir = new DirectoryInfo(account.RootPath);
382                         var fullName = (from folder in dir.EnumerateDirectories()
383                                                         where (folder.Attributes & FileAttributes.Hidden) == 0
384                                                         select folder.FullName).First();
385                         ShowContainerProperties(fullName);            
386                 }
387
388                 public void ShowContainerProperties(string filePath)
389                 {
390                         if (String.IsNullOrWhiteSpace(filePath))
391                                 throw new ArgumentNullException("filePath");
392                         if (!Directory.Exists(filePath))
393                                 throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");
394                         Contract.EndContractBlock();
395
396                         var pair=(from monitor in  Monitors
397                                                            where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
398                                                                    select monitor).FirstOrDefault();
399                     var accountMonitor = pair.Value;            
400                         var info = accountMonitor.GetContainerInfo(filePath);
401
402                         
403
404                         var containerProperties = new ContainerPropertiesViewModel(this, info,filePath);
405                         _windowManager.ShowWindow(containerProperties);
406                 }
407
408         public void SynchNow()
409         {}
410
411                 public ObjectInfo RefreshObjectInfo(ObjectInfo currentInfo)
412                 {
413                         if (currentInfo==null)
414                                 throw new ArgumentNullException("currentInfo");
415                         Contract.EndContractBlock();
416
417                         var monitor = Monitors[currentInfo.Account];
418                         var newInfo=monitor.CloudClient.GetObjectInfo(currentInfo.Account, currentInfo.Container, currentInfo.Name);
419                         return newInfo;
420                 }
421
422                 public ContainerInfo RefreshContainerInfo(ContainerInfo container)
423                 {
424                         if (container == null)
425                                 throw new ArgumentNullException("container");
426                         Contract.EndContractBlock();
427
428                         var monitor = Monitors[container.Account];
429                         var newInfo = monitor.CloudClient.GetContainerInfo(container.Account, container.Name);
430                         return newInfo;
431                 }
432
433
434                 public void ToggleSynching()
435                 {
436                         bool isPaused=false;
437                         foreach (var pair in Monitors)
438                         {
439                                 var monitor = pair.Value;
440                                 monitor.Pause = !monitor.Pause;
441                                 isPaused = monitor.Pause;
442                         }
443
444                         PauseSyncCaption = isPaused ? "Resume syncing" : "Pause syncing";
445                         var iconKey = isPaused? "TraySyncPaused" : "TrayInSynch";
446                         StatusIcon = String.Format(@"../Images/{0}.ico", iconKey);
447                 }
448
449                 public void ExitPithos()
450                 {
451                         foreach (var pair in Monitors)
452                         {
453                                 var monitor = pair.Value;
454                                 monitor.Stop();
455                         }
456
457                         ((Window)GetView()).Close();
458                 }
459                 #endregion
460
461
462                 private readonly Dictionary<PithosStatus, StatusInfo> _iconNames = new List<StatusInfo>
463                         {
464                                 new StatusInfo(PithosStatus.InSynch, "All files up to date", "TrayInSynch"),
465                                 new StatusInfo(PithosStatus.Syncing, "Syncing Files", "TraySynching"),
466                                 new StatusInfo(PithosStatus.SyncPaused, "Sync Paused", "TraySyncPaused")
467                         }.ToDictionary(s => s.Status);
468
469                 readonly IWindowManager _windowManager;
470
471
472                 ///<summary>
473                 /// Updates the visual status indicators of the application depending on status changes, e.g. icon, stat                
474                 ///</summary>
475                 public void UpdateStatus()
476                 {
477                         var pithosStatus = _statusChecker.GetPithosStatus();
478
479                         if (_iconNames.ContainsKey(pithosStatus))
480                         {
481                                 var info = _iconNames[pithosStatus];
482                                 StatusIcon = String.Format(@"../Images/{0}.ico", info.IconName);
483
484                                 Assembly assembly = Assembly.GetExecutingAssembly();                               
485                                 var fileVersion = FileVersionInfo.GetVersionInfo(assembly.Location);
486
487
488                                 StatusMessage = String.Format("Pithos {0}\r\n{1}", fileVersion.FileVersion,info.StatusText);
489                         }
490                         
491                         _events.Publish(new Notification { Title = "Start", Message = "Start Monitoring", Level = TraceLevel.Info});
492                 }
493
494
495            
496                 private Task StartMonitor(PithosMonitor monitor,int retries=0)
497                 {
498                         return Task.Factory.StartNew(() =>
499                         {
500                                 using (log4net.ThreadContext.Stacks["Monitor"].Push("Start"))
501                                 {
502                                         try
503                                         {
504                                                 Log.InfoFormat("Start Monitoring {0}", monitor.UserName);
505
506                                                 monitor.Start();
507                                         }
508                                         catch (WebException exc)
509                                         {
510                                                 if (AbandonRetry(monitor, retries))
511                                                         return;
512
513                         HttpStatusCode statusCode =HttpStatusCode.OK;
514                                     var response = exc.Response as HttpWebResponse;
515                         if(response!=null)
516                                                 statusCode = response.StatusCode;
517
518                         switch (statusCode)
519                         {
520                             case HttpStatusCode.Unauthorized:
521                                 var message = String.Format("API Key Expired for {0}. Starting Renewal",
522                                                             monitor.UserName);
523                                 Log.Error(message, exc);
524                                 TryAuthorize(monitor, retries).Wait();
525                                 break;
526                             case HttpStatusCode.ProxyAuthenticationRequired:
527                                 TryAuthenticateProxy(monitor,retries);
528                                 break;
529                             default:
530                                 TryLater(monitor, exc, retries);
531                                 break;
532                         }
533                                         }
534                                         catch (Exception exc)
535                                         {
536                                                 if (AbandonRetry(monitor, retries)) 
537                                                         return;
538
539                                                 TryLater(monitor,exc,retries);
540                                         }
541                                 }
542                         });
543                 }
544
545             private void TryAuthenticateProxy(PithosMonitor monitor,int retries)
546             {
547                 Execute.OnUIThread(() =>
548                                        {
549                                            var proxyAccount = new ProxyAccountViewModel(this.Settings);
550                                            if (true == _windowManager.ShowDialog(proxyAccount))
551                                            {
552                                            
553                                                StartMonitor(monitor, retries);
554                                                NotifyOfPropertyChange(() => Accounts);
555                                            }
556                                        });
557             }
558
559             private bool AbandonRetry(PithosMonitor monitor, int retries)
560                 {
561                         if (retries > 1)
562                         {
563                                 var message = String.Format("Monitoring of account {0} has failed too many times. Will not retry",
564                                                                                         monitor.UserName);
565                                 _events.Publish(new Notification
566                                                                         {Title = "Account monitoring failed", Message = message, Level = TraceLevel.Error});
567                                 return true;
568                         }
569                         return false;
570                 }
571
572
573                 private async Task TryAuthorize(PithosMonitor monitor,int retries)
574                 {
575                         _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 });
576
577                         try
578                         {
579
580                                 var credentials = await PithosAccount.RetrieveCredentials(Settings.PithosLoginUrl);
581
582                                 var account = Settings.Accounts.First(act => act.AccountName == credentials.UserName);
583                                 account.ApiKey = credentials.Password;
584                                 monitor.ApiKey = credentials.Password;
585                                 Settings.Save();
586                                 await TaskEx.Delay(10000);
587                                 StartMonitor(monitor, retries + 1);
588                                 NotifyOfPropertyChange(()=>Accounts);
589                         }
590                         catch (AggregateException exc)
591                         {
592                                 string message = String.Format("API Key retrieval for {0} failed", monitor.UserName);
593                                 Log.Error(message, exc.InnerException);
594                                 _events.Publish(new Notification { Title = "Authorization failed", Message = message, Level = TraceLevel.Error });
595                         }
596                         catch (Exception exc)
597                         {
598                                 string message = String.Format("API Key retrieval for {0} failed", monitor.UserName);
599                                 Log.Error(message, exc);
600                                 _events.Publish(new Notification { Title = "Authorization failed", Message = message, Level = TraceLevel.Error });
601                         }
602
603                 }
604
605                 private static bool IsUnauthorized(WebException exc)
606                 {
607                         if (exc==null)
608                                 throw new ArgumentNullException("exc");
609                         Contract.EndContractBlock();
610
611                         var response = exc.Response as HttpWebResponse;
612                         if (response == null)
613                                 return false;
614                         return (response.StatusCode == HttpStatusCode.Unauthorized);
615                 }
616
617                 private void TryLater(PithosMonitor monitor, Exception exc,int retries)
618                 {
619                         var message = String.Format("An exception occured. Can't start monitoring\nWill retry in 10 seconds");
620                         Task.Factory.StartNewDelayed(10000, () => StartMonitor(monitor,retries+1));
621                         _events.Publish(new Notification
622                                                                 {Title = "Error", Message = message, Level = TraceLevel.Error});
623                         Log.Error(message, exc);
624                 }
625
626
627                 public void NotifyChange(string status, TraceLevel level=TraceLevel.Info)
628                 {
629                         StatusMessage = status;
630                         
631                         _events.Publish(new Notification { Title = "Pithos", Message = status, Level = level });
632                 }
633
634                 public void NotifyChangedFile(string filePath)
635                 {
636                         var entry = new FileEntry {FullPath=filePath};
637                         IProducerConsumerCollection<FileEntry> files=RecentFiles;
638                         FileEntry popped;
639                         while (files.Count > 5)
640                                 files.TryTake(out popped);
641                         files.TryAdd(entry);
642                 }
643
644                 public void NotifyAccount(AccountInfo account)
645                 {
646                         if (account== null)
647                                 return;
648                         //TODO: What happens to an existing account whose Token has changed?
649                         account.SiteUri= String.Format("{0}/ui/?token={1}&user={2}",
650                                 account.SiteUri, Uri.EscapeUriString(account.Token),
651                                 Uri.EscapeUriString(account.UserName));
652
653                         if (Accounts.All(item => item.UserName != account.UserName))
654                                 Accounts.TryAdd(account);
655
656                 }
657
658
659                 public void RemoveMonitor(string accountName)
660                 {
661                         if (String.IsNullOrWhiteSpace(accountName))
662                                 return;
663
664                         var accountInfo=_accounts.FirstOrDefault(account => account.UserName == accountName);
665                         _accounts.TryRemove(accountInfo);
666
667                         PithosMonitor monitor;
668                         if (Monitors.TryRemove(accountName, out monitor))
669                         {
670                                 monitor.Stop();
671                         }
672                 }
673
674                 public void RefreshOverlays()
675                 {
676                         foreach (var pair in Monitors)
677                         {
678                                 var monitor = pair.Value;
679
680                                 var path = monitor.RootPath;
681
682                                 if (String.IsNullOrWhiteSpace(path))
683                                         continue;
684
685                                 if (!Directory.Exists(path) && !File.Exists(path))
686                                         continue;
687
688                                 IntPtr pathPointer = Marshal.StringToCoTaskMemAuto(path);
689
690                                 try
691                                 {
692                                         NativeMethods.SHChangeNotify(HChangeNotifyEventID.SHCNE_UPDATEITEM,
693                                                                                                  HChangeNotifyFlags.SHCNF_PATHW | HChangeNotifyFlags.SHCNF_FLUSHNOWAIT,
694                                                                                                  pathPointer, IntPtr.Zero);
695                                 }
696                                 finally
697                                 {
698                                         Marshal.FreeHGlobal(pathPointer);
699                                 }
700                         }
701                 }
702
703                 #region Event Handlers
704                 
705                 public void Handle(SelectiveSynchChanges message)
706                 {
707                         var accountName = message.Account.AccountName;
708                         PithosMonitor monitor;
709                         if (_monitors.TryGetValue(accountName, out monitor))
710                         {
711                                 monitor.AddSelectivePaths(message.Added);
712                                 monitor.RemoveSelectivePaths(message.Removed);
713
714                         }
715                         
716                 }
717
718
719                 public void Handle(Notification notification)
720                 {
721                         if (!Settings.ShowDesktopNotifications)
722                                 return;
723                         BalloonIcon icon;
724                         switch (notification.Level)
725                         {
726                                 case TraceLevel.Error:
727                                         icon = BalloonIcon.Error;
728                                         break;
729                                 case TraceLevel.Info:
730                                 case TraceLevel.Verbose:
731                                         icon = BalloonIcon.Info;
732                                         break;
733                                 case TraceLevel.Warning:
734                                         icon = BalloonIcon.Warning;
735                                         break;
736                                 default:
737                                         icon = BalloonIcon.None;
738                                         break;
739                         }
740
741                         if (Settings.ShowDesktopNotifications)
742                         {
743                                 var tv = (ShellView) GetView();
744                                 tv.TaskbarView.ShowBalloonTip(notification.Title, notification.Message, icon);
745                         }
746                 }
747                 #endregion
748
749                 public void Handle(ShowFilePropertiesEvent message)
750                 {
751                         if (message == null)
752                                 throw new ArgumentNullException("message");
753                         if (String.IsNullOrWhiteSpace(message.FileName) )
754                                 throw new ArgumentException("message");
755                         Contract.EndContractBlock();
756
757                         var fileName = message.FileName;
758             //TODO: Display file properties for non-container folders
759                         if (File.Exists(fileName))
760                 //Retrieve the full name with exact casing. Pithos names are case sensitive                             
761                 ShowFileProperties(FileInfoExtensions.GetProperFilePathCapitalization(fileName));
762                         else if (Directory.Exists(fileName))
763                 //Retrieve the full name with exact casing. Pithos names are case sensitive
764                         {
765                 var path = FileInfoExtensions.GetProperDirectoryCapitalization(fileName);
766                 if (IsContainer(path))
767                                 ShowContainerProperties(path);
768                 else
769                     ShowFileProperties(path);
770                         }
771                 }
772
773             private bool IsContainer(string path)
774             {
775                 var matchingFolders = from account in _accounts
776                                       from rootFolder in Directory.GetDirectories(account.AccountPath)
777                                       where rootFolder.Equals(path, StringComparison.InvariantCultureIgnoreCase)
778                                       select rootFolder;
779                 return matchingFolders.Any();
780             }
781
782         }
783 }