/// <summary> /// Initialize all of our behaviors. /// </summary> /// <param name="file"></param> public FileUserControlViewModel(IFile file, IBlobCache cache = null) { File = file; cache = cache ?? Blobs.LocalStorage; // Save the document type for the UI DocumentTypeString = file.FileType.ToUpper(); // Setup the actual file downloader FileDownloader = new FileDownloadController(file, cache); // Now, hook up our UI indicators to the download control. FileDownloader.WhenAny(x => x.IsDownloading, x => x.Value) .ToProperty(this, x => x.IsDownloading, out _isDownloading, false, RxApp.MainThreadScheduler); FileDownloader.WhenAny(x => x.IsDownloaded, x => x.IsDownloading, (x, y) => !x.Value || y.Value) .ToProperty(this, x => x.FileNotCachedOrDownloading, out _fileNotCachedOrDownloading, true, RxApp.MainThreadScheduler); // Allow them to download a file. var canDoDownload = FileDownloader.WhenAny(x => x.IsDownloading, x => x.Value) .Select(x => !x); ClickedUs = ReactiveCommand.Create(canDoDownload); ClickedUs .Where(_ => !FileDownloader.IsDownloaded) .InvokeCommand(FileDownloader.DownloadOrUpdate); // Opening the file is a bit more complex. It happens only when the user clicks the button a second time. // Requires us to write a file to the local cache. ClickedUs .Where(_ => FileDownloader.IsDownloaded) .SelectMany(_ => file.GetFileFromCache(cache)) .SelectMany(async stream => { var fname = string.Format("{0}.{1}", file.DisplayName.CleanFilename(), file.FileType).CleanFilename(); var fdate = await file.GetCacheCreateTime(cache); var folder = fdate.HasValue ? fdate.Value.ToString().CleanFilename() : "Unknown Cache Time"; // Write the file. If it is already written, then we will just return it (e.g. assume it is the same). // 0x800700B7 (-2147024713) is the error code for file already exists. return(CreateFile(folder, fname) .SelectMany(f => f.OpenStreamForWriteAsync()) .SelectMany(async fstream => { try { using (var readerStream = stream.AsStreamForRead()) { await readerStream.CopyToAsync(fstream); } } finally { fstream.Dispose(); } return default(Unit); }) .Catch <Unit, Exception>(e => { if (e.HResult == unchecked ((int)0x800700B7)) { return Observable.Return(default(Unit)); } return Observable.Throw <Unit>(e); }) .SelectMany(_ => GetExistingFile(folder, fname))); }) .SelectMany(f => f) .ObserveOn(RxApp.MainThreadScheduler) .SelectMany(f => { return(Observable.FromAsync(async _ => await Launcher.LaunchFileAsync(f)) .Select(good => Tuple.Create(f, good)) .Catch(Observable.Return(Tuple.Create(f, false)))); }) .Where(g => g.Item2 == false) .ObserveOn(RxApp.MainThreadScheduler) .SelectMany(f => { return(Observable.FromAsync(async _ => await Launcher.LaunchFileAsync(f.Item1, new LauncherOptions() { DisplayApplicationPicker = true })) .Catch(Observable.Return(false))); }) .Where(g => g == false) .Subscribe( g => { throw new InvalidOperationException(string.Format("Unable to open file {0}.", file.DisplayName)); }, e => { throw new InvalidOperationException(string.Format("Unable to open file {0}.", file.DisplayName), e); } ); // Init the UI from the cache. We want to do one or the other // because the download will fetch from the cache first. So no need to // fire them both off. OnLoaded = ReactiveCommand.Create(); OnLoaded .Where(_ => Settings.AutoDownloadNewMeeting) .InvokeCommand(FileDownloader.DownloadOrUpdate); }
/// <summary> /// Get ourselves setup and going given a file source. /// </summary> /// <param name="fileSource"></param> public PDFFile(FileDownloadController fileSource) { // Each time a new file shows up, get the file and decode it. var isDownloaded = fileSource .WhenAny(x => x.IsDownloaded, x => x.Value) .Where(dwn => dwn == true) .Select(_ => default(Unit)); var newFile = fileSource .FileDownloadedAndCached; // Load it up as a real PDF document. Make sure we don't do it more than once. // Note the publish below - otherwise we will miss it going by if it happens too // fast. var cacheKey = Observable.Merge(isDownloaded, newFile) .SelectMany(_ => fileSource.File.GetCacheCreateTime(fileSource.Cache)) .Select(date => string.Format("{0}-{1}", fileSource.File.UniqueKey, date.ToString())) .DistinctUntilChanged(); // This will render a document each time it is called. Note the // the Replay at the end. We want to use the same file for everyone. And, each time // a new file comes through, the cacheKey should be updated, and that should cause // this to be re-subscribed. So this is good ONLY FOR ONE FILE at a time. Re-subscribe to // get a new version of the file. // -> Check that we don't need a RefCount - if we did, we'd have to be careful that getting the # of pages // didn't cause one load, and then the rendering caused another load. The sequence might matter... // -> The Take(1) is to make sure we do this only once. Otherwise this sequence could remain open forever, // and that will cause problems with the GetOrFetchObject, which expects to use only the last time in the sequence // it looks at! Func <IObservable <PdfDocument> > pdfObservableFactory = () => fileSource.WhenAny(x => x.IsDownloaded, x => x.Value) .Where(downhere => downhere == true) .Take(1) .SelectMany(_ => fileSource.File.GetFileFromCache(fileSource.Cache)) .SelectMany(stream => PdfDocument.LoadFromStreamAsync(stream)) .Catch <PdfDocument, Exception>(ex => { Debug.WriteLine("The PDF rendering failed: {0}", ex.Message); return(Observable.Empty <PdfDocument>()); }) .PublishLast().ConnectAfterSubscription(); // Finally, build the combination of these two guys. // Make sure that we don't keep re-creating this. We want to make sure // that only one version of the file (from pdfObservableFactory) is // generated. So do a Publish at the end here. var ck = cacheKey .Select(key => Tuple.Create(key, pdfObservableFactory())).Replay(1); _pdfAndCacheKey = ck; // The number of pages is complex in that we will need to fetch the file and render it if we've not already // cached it. Func <IObservable <PdfDocument>, IObservable <int> > fetchNumberOfPages = docs => docs.Select(d => (int)d.PageCount); _pdfAndCacheKey .SelectMany(info => fileSource.Cache.GetOrFetchObject(string.Format("{0}-NumberOfPages", info.Item1), () => fetchNumberOfPages(info.Item2), DateTime.Now + Settings.CacheFilesTime)) .ToProperty(this, x => x.NumberOfPages, out _nPages, 0); // TODO: this should probably be a RefCount - otherwise this right here causes fetches // from all sorts of places (like the cache). Won't trigger a download, so it isn't too bad. ck.Connect(); }