Statistics
| Branch: | Revision:

root / trunk / Pithos.Client.WPF / Shell / ShellViewModel.cs @ 422c9598

History | View | Annotate | Download (25.8 kB)

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 System.Windows.Controls.Primitives;
12
using Caliburn.Micro;
13
using Hardcodet.Wpf.TaskbarNotification;
14
using Pithos.Client.WPF.Configuration;
15
using Pithos.Client.WPF.FileProperties;
16
using Pithos.Client.WPF.Preferences;
17
using Pithos.Client.WPF.SelectiveSynch;
18
using Pithos.Client.WPF.Services;
19
using Pithos.Client.WPF.Shell;
20
using Pithos.Core;
21
using Pithos.Core.Agents;
22
using Pithos.Interfaces;
23
using System;
24
using System.Collections.Generic;
25
using System.Linq;
26
using Pithos.Network;
27
using StatusService = Pithos.Client.WPF.Services.StatusService;
28

    
29
namespace Pithos.Client.WPF {
30
	using System.ComponentModel.Composition;
31

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

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

    
56

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

    
72

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

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

    
82
        //Lazily initialized File Version info. This is done once and lazily to avoid blocking the UI
83
        private Lazy<FileVersionInfo> _fileVersion;
84

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

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

    
105
				Settings = settings;
106

    
107
				StatusMessage = "In Synch";
108

    
109
                _fileVersion=  new Lazy<FileVersionInfo>(() =>
110
                {
111
                    Assembly assembly = Assembly.GetExecutingAssembly();
112
                    var fileVersion = FileVersionInfo.GetVersionInfo(assembly.Location);
113
                    return fileVersion;
114
                });
115
				_accounts.CollectionChanged += (sender, e) =>
116
												   {
117
													   NotifyOfPropertyChange(() => OpenFolderCaption);
118
													   NotifyOfPropertyChange(() => HasAccounts);
119
												   };
120

    
121
			}
122
			catch (Exception exc)
123
			{
124
				Log.Error("Error while starting the ShellViewModel",exc);
125
				throw;
126
			}
127
		}
128

    
129

    
130
		protected override void OnActivate()
131
		{
132
			base.OnActivate();
133

    
134
			StartMonitoring();                    
135
		}
136

    
137

    
138
		private async void StartMonitoring()
139
		{
140
			try
141
			{
142
				var accounts = Settings.Accounts.Select(MonitorAccount);
143
				await TaskEx.WhenAll(accounts);
144
				_statusService = StatusService.Start();
145

    
146
/*
147
				foreach (var account in Settings.Accounts)
148
				{
149
					await MonitorAccount(account);
150
				}
151
*/
152
				
153
			}
154
			catch (AggregateException exc)
155
			{
156
				exc.Handle(e =>
157
				{
158
					Log.Error("Error while starting monitoring", e);
159
					return true;
160
				});
161
				throw;
162
			}
163
		}
164

    
165
		protected override void OnDeactivate(bool close)
166
		{
167
			base.OnDeactivate(close);
168
			if (close)
169
			{
170
				StatusService.Stop(_statusService);
171
				_statusService = null;
172
			}
173
		}
174

    
175
		public Task MonitorAccount(AccountSettings account)
176
		{
177
			return Task.Factory.StartNew(() =>
178
			{                                                
179
				PithosMonitor monitor;
180
				var accountName = account.AccountName;
181

    
182
				if (_monitors.TryGetValue(accountName, out monitor))
183
				{
184
					//If the account is active
185
					if (account.IsActive)
186
						//Start the monitor. It's OK to start an already started monitor,
187
						//it will just ignore the call                        
188
						StartMonitor(monitor).Wait();                        
189
					else
190
					{
191
						//If the account is inactive
192
						//Stop and remove the monitor
193
						RemoveMonitor(accountName);
194
					}
195
					return;
196
				}
197

    
198
                
199
				//Create a new monitor/ Can't use MEF here, it would return a single instance for all monitors
200
				monitor = new PithosMonitor
201
							  {
202
								  UserName = accountName,
203
								  ApiKey = account.ApiKey,                                  
204
								  StatusNotification = this,
205
								  RootPath = account.RootPath
206
							  };
207
				//PithosMonitor uses MEF so we need to resolve it
208
				IoC.BuildUp(monitor);
209

    
210
			    monitor.AuthenticationUrl = account.ServerUrl;
211

    
212
				_monitors[accountName] = monitor;
213

    
214
				if (account.IsActive)
215
				{
216
					//Don't start a monitor if it doesn't have an account and ApiKey
217
					if (String.IsNullOrWhiteSpace(monitor.UserName) ||
218
						String.IsNullOrWhiteSpace(monitor.ApiKey))
219
						return;
220
					StartMonitor(monitor);
221
				}
222
			});
223
		}
224

    
225

    
226
		protected override void OnViewLoaded(object view)
227
		{
228
			UpdateStatus();
229
			var window = (Window)view;            
230
			TaskEx.Delay(1000).ContinueWith(t => Execute.OnUIThread(window.Hide));
231
			base.OnViewLoaded(view);
232
		}
233

    
234

    
235
		#region Status Properties
236

    
237
		private string _statusMessage;
238
		public string StatusMessage
239
		{
240
			get { return _statusMessage; }
241
			set
242
			{
243
				_statusMessage = value;
244
				NotifyOfPropertyChange(() => StatusMessage);
245
			}
246
		}
247

    
248
		private readonly ObservableConcurrentCollection<AccountInfo> _accounts = new ObservableConcurrentCollection<AccountInfo>();
249
		public ObservableConcurrentCollection<AccountInfo> Accounts
250
		{
251
			get { return _accounts; }
252
		}
253

    
254
		public bool HasAccounts
255
		{
256
			get { return _accounts.Count > 0; }
257
		}
258

    
259

    
260
		public string OpenFolderCaption
261
		{
262
			get
263
			{
264
				return (_accounts.Count == 0)
265
						? "No Accounts Defined"
266
						: "Open Pithos Folder";
267
			}
268
		}
269

    
270
		private string _pauseSyncCaption="Pause Synching";
271
		public string PauseSyncCaption
272
		{
273
			get { return _pauseSyncCaption; }
274
			set
275
			{
276
				_pauseSyncCaption = value;
277
				NotifyOfPropertyChange(() => PauseSyncCaption);
278
			}
279
		}
280

    
281
		private readonly ObservableConcurrentCollection<FileEntry> _recentFiles = new ObservableConcurrentCollection<FileEntry>();
282
		public ObservableConcurrentCollection<FileEntry> RecentFiles
283
		{
284
			get { return _recentFiles; }
285
		}
286

    
287

    
288
		private string _statusIcon="../Images/Pithos.ico";
289
		public string StatusIcon
290
		{
291
			get { return _statusIcon; }
292
			set
293
			{
294
                //TODO: Ensure all status icons use the Pithos logo
295
				_statusIcon = value;
296
				NotifyOfPropertyChange(() => StatusIcon);
297
			}
298
		}
299

    
300
		#endregion
301

    
302
		#region Commands
303

    
304
		public void ShowPreferences()
305
		{
306
			Settings.Reload();
307
			var preferences = new PreferencesViewModel(_windowManager,_events, this,Settings);            
308
			_windowManager.ShowDialog(preferences);
309
			
310
		}
311

    
312
		public void AboutPithos()
313
		{
314
			var about = new AboutViewModel();
315
			_windowManager.ShowWindow(about);
316
		}
317

    
318
		public void SendFeedback()
319
		{
320
			var feedBack =  IoC.Get<FeedbackViewModel>();
321
			_windowManager.ShowWindow(feedBack);
322
		}
323

    
324
		//public PithosCommand OpenPithosFolderCommand { get; private set; }
325

    
326
		public void OpenPithosFolder()
327
		{
328
			var account = Settings.Accounts.FirstOrDefault(acc => acc.IsActive);
329
			if (account == null)
330
				return;
331
			Process.Start(account.RootPath);
332
		}
333

    
334
		public void OpenPithosFolder(AccountInfo account)
335
		{
336
			Process.Start(account.AccountPath);
337
		}
338

    
339
		
340
/*
341
		public void GoToSite()
342
		{            
343
			var site = Properties.Settings.Default.PithosSite;
344
			Process.Start(site);            
345
		}
346
*/
347

    
348
		public void GoToSite(AccountInfo account)
349
		{
350
			/*var site = String.Format("{0}/ui/?token={1}&user={2}",
351
				account.SiteUri,account.Token,
352
				account.UserName);*/
353
			Process.Start(account.SiteUri);
354
		}
355

    
356
		public void ShowFileProperties()
357
		{
358
			var account = Settings.Accounts.First(acc => acc.IsActive);            
359
			var dir = new DirectoryInfo(account.RootPath + @"\pithos");
360
			var files=dir.GetFiles();
361
			var r=new Random();
362
			var idx=r.Next(0, files.Length);
363
			ShowFileProperties(files[idx].FullName);            
364
		}
365

    
366
		public void ShowFileProperties(string filePath)
367
		{
368
			if (String.IsNullOrWhiteSpace(filePath))
369
				throw new ArgumentNullException("filePath");
370
			if (!File.Exists(filePath) && !Directory.Exists(filePath))
371
				throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");
372
			Contract.EndContractBlock();
373

    
374
			var pair=(from monitor in  Monitors
375
							   where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
376
								   select monitor).FirstOrDefault();
377
		    var accountMonitor = pair.Value;
378

    
379
			if (accountMonitor == null)
380
				return;
381

    
382
			var infoTask=Task.Factory.StartNew(()=>accountMonitor.GetObjectInfo(filePath));
383

    
384
			
385

    
386
			var fileProperties = new FilePropertiesViewModel(this, infoTask,filePath);
387
			_windowManager.ShowWindow(fileProperties);
388
		} 
389
		
390
		public void ShowContainerProperties()
391
		{
392
			var account = Settings.Accounts.First(acc => acc.IsActive);            
393
			var dir = new DirectoryInfo(account.RootPath);
394
			var fullName = (from folder in dir.EnumerateDirectories()
395
							where (folder.Attributes & FileAttributes.Hidden) == 0
396
							select folder.FullName).First();
397
			ShowContainerProperties(fullName);            
398
		}
399

    
400
		public void ShowContainerProperties(string filePath)
401
		{
402
			if (String.IsNullOrWhiteSpace(filePath))
403
				throw new ArgumentNullException("filePath");
404
			if (!Directory.Exists(filePath))
405
				throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");
406
			Contract.EndContractBlock();
407

    
408
			var pair=(from monitor in  Monitors
409
							   where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
410
								   select monitor).FirstOrDefault();
411
		    var accountMonitor = pair.Value;            
412
			var info = accountMonitor.GetContainerInfo(filePath);
413

    
414
			
415

    
416
			var containerProperties = new ContainerPropertiesViewModel(this, info,filePath);
417
			_windowManager.ShowWindow(containerProperties);
418
		}
419

    
420
        public void SynchNow()
421
        {
422
            var agent = IoC.Get<NetworkAgent>();
423
            agent.SynchNow();
424
        }
425

    
426
		public ObjectInfo RefreshObjectInfo(ObjectInfo currentInfo)
427
		{
428
			if (currentInfo==null)
429
				throw new ArgumentNullException("currentInfo");
430
			Contract.EndContractBlock();
431

    
432
			var monitor = Monitors[currentInfo.Account];
433
			var newInfo=monitor.CloudClient.GetObjectInfo(currentInfo.Account, currentInfo.Container, currentInfo.Name);
434
			return newInfo;
435
		}
436

    
437
		public ContainerInfo RefreshContainerInfo(ContainerInfo container)
438
		{
439
			if (container == null)
440
				throw new ArgumentNullException("container");
441
			Contract.EndContractBlock();
442

    
443
			var monitor = Monitors[container.Account];
444
			var newInfo = monitor.CloudClient.GetContainerInfo(container.Account, container.Name);
445
			return newInfo;
446
		}
447

    
448

    
449
		public void ToggleSynching()
450
		{
451
			bool isPaused=false;
452
			foreach (var pair in Monitors)
453
			{
454
				var monitor = pair.Value;
455
				monitor.Pause = !monitor.Pause;
456
				isPaused = monitor.Pause;
457
			}
458

    
459
			PauseSyncCaption = isPaused ? "Resume syncing" : "Pause syncing";
460
			var iconKey = isPaused? "TraySyncPaused" : "TrayInSynch";
461
			StatusIcon = String.Format(@"../Images/{0}.ico", iconKey);
462
		}
463

    
464
		public void ExitPithos()
465
		{
466
			foreach (var pair in Monitors)
467
			{
468
				var monitor = pair.Value;
469
				monitor.Stop();
470
			}
471

    
472
			((Window)GetView()).Close();
473
		}
474
		#endregion
475

    
476

    
477
		private readonly Dictionary<PithosStatus, StatusInfo> _iconNames = new List<StatusInfo>
478
			{
479
				new StatusInfo(PithosStatus.InSynch, "All files up to date", "TrayInSynch"),
480
				new StatusInfo(PithosStatus.Syncing, "Syncing Files", "TraySynching"),
481
				new StatusInfo(PithosStatus.SyncPaused, "Sync Paused", "TraySyncPaused")
482
			}.ToDictionary(s => s.Status);
483

    
484
		readonly IWindowManager _windowManager;
485
	    
486

    
487
		///<summary>
488
		/// Updates the visual status indicators of the application depending on status changes, e.g. icon, stat		
489
		///</summary>
490
		public void UpdateStatus()
491
		{
492
			var pithosStatus = _statusChecker.GetPithosStatus();
493

    
494
			if (_iconNames.ContainsKey(pithosStatus))
495
			{
496
				var info = _iconNames[pithosStatus];
497
				StatusIcon = String.Format(@"../Images/{0}.ico", info.IconName);
498

    
499

    
500

    
501
				StatusMessage = String.Format("Pithos {0}\r\n{1}", _fileVersion.Value.FileVersion,info.StatusText);
502
			}
503
			
504
			//_events.Publish(new Notification { Title = "Start", Message = "Start Monitoring", Level = TraceLevel.Info});
505
		}
506

    
507

    
508
	   
509
		private Task StartMonitor(PithosMonitor monitor,int retries=0)
510
		{
511
			return Task.Factory.StartNew(() =>
512
			{
513
				using (log4net.ThreadContext.Stacks["Monitor"].Push("Start"))
514
				{
515
					try
516
					{
517
						Log.InfoFormat("Start Monitoring {0}", monitor.UserName);
518

    
519
						monitor.Start();
520
					}
521
					catch (WebException exc)
522
					{
523
						if (AbandonRetry(monitor, retries))
524
							return;
525

    
526
                        HttpStatusCode statusCode =HttpStatusCode.OK;
527
			            var response = exc.Response as HttpWebResponse;
528
                        if(response!=null)
529
					        statusCode = response.StatusCode;
530

    
531
                        switch (statusCode)
532
                        {
533
                            case HttpStatusCode.Unauthorized:
534
                                var message = String.Format("API Key Expired for {0}. Starting Renewal",
535
                                                            monitor.UserName);
536
                                Log.Error(message, exc);
537
                                TryAuthorize(monitor, retries).Wait();
538
                                break;
539
                            case HttpStatusCode.ProxyAuthenticationRequired:
540
                                TryAuthenticateProxy(monitor,retries);
541
                                break;
542
                            default:
543
                                TryLater(monitor, exc, retries);
544
                                break;
545
                        }
546
					}
547
					catch (Exception exc)
548
					{
549
						if (AbandonRetry(monitor, retries)) 
550
							return;
551

    
552
						TryLater(monitor,exc,retries);
553
					}
554
				}
555
			});
556
		}
557

    
558
	    private void TryAuthenticateProxy(PithosMonitor monitor,int retries)
559
	    {
560
	        Execute.OnUIThread(() =>
561
	                               {
562
	                                   var proxyAccount = new ProxyAccountViewModel(this.Settings);
563
	                                   if (true == _windowManager.ShowDialog(proxyAccount))
564
	                                   {
565
                                           
566
	                                       StartMonitor(monitor, retries);
567
	                                       NotifyOfPropertyChange(() => Accounts);
568
	                                   }
569
	                               });
570
	    }
571

    
572
	    private bool AbandonRetry(PithosMonitor monitor, int retries)
573
		{
574
			if (retries > 1)
575
			{
576
				var message = String.Format("Monitoring of account {0} has failed too many times. Will not retry",
577
											monitor.UserName);
578
				_events.Publish(new Notification
579
									{Title = "Account monitoring failed", Message = message, Level = TraceLevel.Error});
580
				return true;
581
			}
582
			return false;
583
		}
584

    
585

    
586
		private async Task TryAuthorize(PithosMonitor monitor,int retries)
587
		{
588
			_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 });
589

    
590
			try
591
			{
592

    
593
				var credentials = await PithosAccount.RetrieveCredentials(Settings.PithosLoginUrl);
594

    
595
				var account = Settings.Accounts.First(act => act.AccountName == credentials.UserName);
596
				account.ApiKey = credentials.Password;
597
				monitor.ApiKey = credentials.Password;
598
				Settings.Save();
599
				await TaskEx.Delay(10000);
600
				StartMonitor(monitor, retries + 1);
601
				NotifyOfPropertyChange(()=>Accounts);
602
			}
603
			catch (AggregateException exc)
604
			{
605
				string message = String.Format("API Key retrieval for {0} failed", monitor.UserName);
606
				Log.Error(message, exc.InnerException);
607
				_events.Publish(new Notification { Title = "Authorization failed", Message = message, Level = TraceLevel.Error });
608
			}
609
			catch (Exception exc)
610
			{
611
				string message = String.Format("API Key retrieval for {0} failed", monitor.UserName);
612
				Log.Error(message, exc);
613
				_events.Publish(new Notification { Title = "Authorization failed", Message = message, Level = TraceLevel.Error });
614
			}
615

    
616
		}
617

    
618
		private static bool IsUnauthorized(WebException exc)
619
		{
620
			if (exc==null)
621
				throw new ArgumentNullException("exc");
622
			Contract.EndContractBlock();
623

    
624
			var response = exc.Response as HttpWebResponse;
625
			if (response == null)
626
				return false;
627
			return (response.StatusCode == HttpStatusCode.Unauthorized);
628
		}
629

    
630
		private void TryLater(PithosMonitor monitor, Exception exc,int retries)
631
		{
632
			var message = String.Format("An exception occured. Can't start monitoring\nWill retry in 10 seconds");
633
			Task.Factory.StartNewDelayed(10000, () => StartMonitor(monitor,retries+1));
634
			_events.Publish(new Notification
635
								{Title = "Error", Message = message, Level = TraceLevel.Error});
636
			Log.Error(message, exc);
637
		}
638

    
639

    
640
		public void NotifyChange(string status, TraceLevel level=TraceLevel.Info)
641
		{
642
			StatusMessage = status;
643
			
644
			_events.Publish(new Notification { Title = "Pithos", Message = status, Level = level });
645
		}
646

    
647
		public void NotifyChangedFile(string filePath)
648
		{
649
			var entry = new FileEntry {FullPath=filePath};
650
			IProducerConsumerCollection<FileEntry> files=RecentFiles;
651
			FileEntry popped;
652
			while (files.Count > 5)
653
				files.TryTake(out popped);
654
			files.TryAdd(entry);
655
		}
656

    
657
		public void NotifyAccount(AccountInfo account)
658
		{
659
			if (account== null)
660
				return;
661
			//TODO: What happens to an existing account whose Token has changed?
662
			account.SiteUri= String.Format("{0}/ui/?token={1}&user={2}",
663
				account.SiteUri, Uri.EscapeDataString(account.Token),
664
                Uri.EscapeDataString(account.UserName));
665

    
666
			if (Accounts.All(item => item.UserName != account.UserName))
667
				Accounts.TryAdd(account);
668

    
669
		}
670

    
671
	    public void NotifyConflicts(IEnumerable<FileSystemInfo> conflictFiles, string message)
672
	    {
673
            if (conflictFiles == null)
674
                return;
675
            if (!conflictFiles.Any())
676
                return;
677

    
678
            UpdateStatus();
679
            //TODO: Create a more specific message. For now, just show a warning
680
            NotifyForFiles(conflictFiles,message,TraceLevel.Warning);
681

    
682
	    }
683

    
684
	    public void NotifyForFiles(IEnumerable<FileSystemInfo> files, string message,TraceLevel level=TraceLevel.Info)
685
	    {
686
            if (files == null)
687
                return;
688
            if (!files.Any())
689
                return;
690

    
691
            StatusMessage = message;
692

    
693
            _events.Publish(new Notification { Title = "Pithos", Message = message, Level = level});
694
        }
695

    
696
        public void Notify(Notification notification)
697
        {
698
            _events.Publish(notification);
699
        }
700

    
701

    
702
	    public void RemoveMonitor(string accountName)
703
		{
704
			if (String.IsNullOrWhiteSpace(accountName))
705
				return;
706

    
707
			var accountInfo=_accounts.FirstOrDefault(account => account.UserName == accountName);
708
			_accounts.TryRemove(accountInfo);
709

    
710
			PithosMonitor monitor;
711
			if (Monitors.TryRemove(accountName, out monitor))
712
			{
713
				monitor.Stop();
714
			}
715
		}
716

    
717
		public void RefreshOverlays()
718
		{
719
			foreach (var pair in Monitors)
720
			{
721
				var monitor = pair.Value;
722

    
723
				var path = monitor.RootPath;
724

    
725
				if (String.IsNullOrWhiteSpace(path))
726
					continue;
727

    
728
				if (!Directory.Exists(path) && !File.Exists(path))
729
					continue;
730

    
731
				IntPtr pathPointer = Marshal.StringToCoTaskMemAuto(path);
732

    
733
				try
734
				{
735
					NativeMethods.SHChangeNotify(HChangeNotifyEventID.SHCNE_UPDATEITEM,
736
												 HChangeNotifyFlags.SHCNF_PATHW | HChangeNotifyFlags.SHCNF_FLUSHNOWAIT,
737
												 pathPointer, IntPtr.Zero);
738
				}
739
				finally
740
				{
741
					Marshal.FreeHGlobal(pathPointer);
742
				}
743
			}
744
		}
745

    
746
		#region Event Handlers
747
		
748
		public void Handle(SelectiveSynchChanges message)
749
		{
750
			var accountName = message.Account.AccountName;
751
			PithosMonitor monitor;
752
			if (_monitors.TryGetValue(accountName, out monitor))
753
			{
754
				monitor.AddSelectivePaths(message.Added);
755
				monitor.RemoveSelectivePaths(message.Removed);
756

    
757
			}
758
			
759
		}
760

    
761

    
762
	    private bool _pollStarted = false;
763

    
764
        //SMELL: Doing so much work for notifications in the shell is wrong
765
        //The notifications should be moved to their own view/viewmodel pair
766
        //and different templates should be used for different message types
767
        //This will also allow the addition of extra functionality, eg. actions
768
        //
769
		public void Handle(Notification notification)
770
		{
771
            UpdateStatus();
772

    
773
			if (!Settings.ShowDesktopNotifications)
774
				return;
775

    
776
            if (notification is PollNotification)
777
            {
778
                _pollStarted = true;
779
                return;
780
            }
781
            if (notification is CloudNotification)
782
            {
783
                if (!_pollStarted) 
784
                    return;
785
                _pollStarted= false;
786
                notification.Title = "Pithos";
787
                notification.Message = "Start Synchronisation";
788
            }
789

    
790
            if (String.IsNullOrWhiteSpace(notification.Message) && String.IsNullOrWhiteSpace(notification.Title))
791
                return;
792

    
793
			BalloonIcon icon;
794
			switch (notification.Level)
795
			{
796
				case TraceLevel.Error:
797
					icon = BalloonIcon.Error;
798
					break;
799
				case TraceLevel.Info:
800
				case TraceLevel.Verbose:
801
					icon = BalloonIcon.Info;
802
					break;
803
				case TraceLevel.Warning:
804
					icon = BalloonIcon.Warning;
805
					break;
806
				default:
807
					icon = BalloonIcon.None;
808
					break;
809
			}
810
            
811
			if (Settings.ShowDesktopNotifications)
812
			{
813
				var tv = (ShellView) GetView();                
814
                var balloon=new PithosBalloon{Title=notification.Title,Message=notification.Message,Icon=icon};
815
                tv.TaskbarView.ShowCustomBalloon(balloon,PopupAnimation.Fade,4000);
816
//				tv.TaskbarView.ShowBalloonTip(notification.Title, notification.Message, icon);
817
			}
818
		}
819
		#endregion
820

    
821
		public void Handle(ShowFilePropertiesEvent message)
822
		{
823
			if (message == null)
824
				throw new ArgumentNullException("message");
825
			if (String.IsNullOrWhiteSpace(message.FileName) )
826
				throw new ArgumentException("message");
827
			Contract.EndContractBlock();
828

    
829
			var fileName = message.FileName;
830
            //TODO: Display file properties for non-container folders
831
			if (File.Exists(fileName))
832
                //Retrieve the full name with exact casing. Pithos names are case sensitive				
833
                ShowFileProperties(FileInfoExtensions.GetProperFilePathCapitalization(fileName));
834
			else if (Directory.Exists(fileName))
835
                //Retrieve the full name with exact casing. Pithos names are case sensitive
836
			{
837
                var path = FileInfoExtensions.GetProperDirectoryCapitalization(fileName);
838
                if (IsContainer(path))
839
			        ShowContainerProperties(path);
840
                else
841
                    ShowFileProperties(path);
842
			}
843
		}
844

    
845
	    private bool IsContainer(string path)
846
	    {
847
	        var matchingFolders = from account in _accounts
848
	                              from rootFolder in Directory.GetDirectories(account.AccountPath)
849
	                              where rootFolder.Equals(path, StringComparison.InvariantCultureIgnoreCase)
850
	                              select rootFolder;
851
	        return matchingFolders.Any();
852
	    }
853

    
854
	    public FileStatus GetFileStatus(string localFileName)
855
	    {
856
            if (String.IsNullOrWhiteSpace(localFileName))
857
                throw new ArgumentNullException("localFileName");
858
            Contract.EndContractBlock();
859
            
860
	        var statusKeeper = IoC.Get<IStatusKeeper>();
861
	        var status=statusKeeper.GetFileStatus(localFileName);
862
	        return status;
863
	    }
864
	}
865
}