diff --git a/ReactiveUI/Cocoa/AutoLayoutViewModelViewHost.cs b/ReactiveUI/Cocoa/AutoLayoutViewModelViewHost.cs index 2e6bd5c995..5b33b6ab6f 100644 --- a/ReactiveUI/Cocoa/AutoLayoutViewModelViewHost.cs +++ b/ReactiveUI/Cocoa/AutoLayoutViewModelViewHost.cs @@ -1,3 +1,4 @@ +using System; #if UNIFIED && UIKIT using NSView = UIKit.UIView; #elif UNIFIED && COCOA @@ -16,9 +17,10 @@ namespace ReactiveUI /// up edge constraints for you from the parent view (the target) /// to the child subview. /// - public class AutoLayoutViewModelViewHost : ViewModelViewHost + [Obsolete("Use ViewModelViewHost instead. This class will be removed in a future release.")] + public class AutoLayoutViewModelViewHostLegacy : ViewModelViewHostLegacy { - public AutoLayoutViewModelViewHost(NSView targetView) : base(targetView) + public AutoLayoutViewModelViewHostLegacy(NSView targetView) : base(targetView) { AddAutoLayoutConstraintsToSubView = true; } diff --git a/ReactiveUI/Cocoa/ViewModelViewHost.cs b/ReactiveUI/Cocoa/ViewModelViewHost.cs index 499c70d733..7f79ee0bf4 100644 --- a/ReactiveUI/Cocoa/ViewModelViewHost.cs +++ b/ReactiveUI/Cocoa/ViewModelViewHost.cs @@ -1,6 +1,7 @@ using System; using System.Reactive.Linq; using ReactiveUI; +using System.Reactive.Disposables; #if UNIFIED && UIKIT using UIKit; @@ -18,6 +19,152 @@ namespace ReactiveUI { + public class ViewModelViewHost : ReactiveViewController + { + private readonly SerialDisposable currentView; + private readonly ObservableAsPropertyHelper viewContract; + private IViewLocator viewLocator; + private NSViewController defaultContent; + private IReactiveObject viewModel; + private IObservable viewContractObservable; + + public ViewModelViewHost() + { + this.currentView = new SerialDisposable(); + this.viewContract = this + .WhenAnyObservable(x => x.ViewContractObservable) + .ToProperty(this, x => x.ViewContract, scheduler: RxApp.MainThreadScheduler); + + this.Initialize(); + } + + public IViewLocator ViewLocator + { + get { return viewLocator; } + set { this.RaiseAndSetIfChanged(ref viewLocator, value); } + } + + public NSViewController DefaultContent + { + get { return defaultContent; } + set { this.RaiseAndSetIfChanged(ref defaultContent, value); } + } + + public IReactiveObject ViewModel + { + get { return viewModel; } + set { this.RaiseAndSetIfChanged(ref viewModel, value); } + } + + public IObservable ViewContractObservable + { + get { return viewContractObservable; } + set { this.RaiseAndSetIfChanged(ref viewContractObservable, value); } + } + + public string ViewContract + { + get { return this.viewContract.Value; } + set { ViewContractObservable = Observable.Return(value); } + } + + private void Initialize() + { + var viewChange = Observable + .CombineLatest( + this.WhenAnyValue(x => x.ViewModel), + this.WhenAnyObservable(x => x.ViewContractObservable).StartWith((string)null), + (vm, contract) => new { ViewModel = vm, Contract = contract }) + .Where(x => x.ViewModel != null); + + var defaultViewChange = Observable + .CombineLatest( + this.WhenAnyValue(x => x.ViewModel), + this.WhenAnyValue(x => x.DefaultContent), + (vm, defaultContent) => new { ViewModel = vm, DefaultContent = defaultContent }) + .Where(x => x.ViewModel == null && x.DefaultContent != null) + .Select(x => x.DefaultContent); + + viewChange + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe( + x => + { + var viewLocator = ViewLocator ?? ReactiveUI.ViewLocator.Current; + var view = viewLocator.ResolveView(x.ViewModel, x.Contract); + + if (view == null) + { + var message = string.Format("Unable to resolve view for \"{0}\"", x.ViewModel.GetType()); + + if (x.Contract != null) + { + message += string.Format(" and contract \"{0}\"", x.Contract.GetType()); + } + + message += "."; + throw new Exception(message); + } + + var viewController = view as NSViewController; + + if (viewController == null) + { + throw new Exception( + string.Format( + "Resolved view type '{0}' is not a '{1}'.", + viewController.GetType().FullName, + typeof(NSViewController).FullName)); + } + + view.ViewModel = x.ViewModel; + Adopt(this, viewController); + + var disposables = new CompositeDisposable(); + disposables.Add(viewController); + disposables.Add(Disposable.Create(() => Disown(viewController))); + currentView.Disposable = disposables; + }); + + defaultViewChange + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(x => Adopt(this, x)); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (disposing) + { + this.currentView.Dispose(); + this.viewContract.Dispose(); + } + } + + private static void Adopt(UIViewController parent, UIViewController child) + { + parent.AddChildViewController(child); + parent.View.AddSubview(child.View); + + // ensure the child view fills our entire frame + child.View.Frame = parent.View.Bounds; + child.View.AutoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight; + child.View.TranslatesAutoresizingMaskIntoConstraints = true; + + child.DidMoveToParentViewController(parent); + } + + private static void Disown(UIViewController child) + { + child.WillMoveToParentViewController(null); + child.View.RemoveFromSuperview(); + child.RemoveFromParentViewController(); + } + } + + + /// /// ViewModelViewHost is a helper class that will connect a ViewModel /// to an arbitrary NSView and attempt to load the View for the current @@ -26,7 +173,8 @@ namespace ReactiveUI /// This is a bit different than the XAML's ViewModelViewHost in the sense /// that this isn't a Control itself, it only manipulates other Views. /// - public class ViewModelViewHost : ReactiveObject + [Obsolete("Use ViewModelViewHost instead. This class will be removed in a later release.")] + public class ViewModelViewHostLegacy : ReactiveObject { /// /// Gets or sets a value indicating whether this @@ -35,7 +183,7 @@ public class ViewModelViewHost : ReactiveObject /// true if add layout contraints to sub view; otherwise, false. public bool AddAutoLayoutConstraintsToSubView { get; set; } - public ViewModelViewHost(NSView targetView) + public ViewModelViewHostLegacy(NSView targetView) { if (targetView == null) throw new ArgumentNullException("targetView"); diff --git a/ReactiveUI/ObservableAsPropertyHelper.cs b/ReactiveUI/ObservableAsPropertyHelper.cs index b7affafcc7..5360f44877 100644 --- a/ReactiveUI/ObservableAsPropertyHelper.cs +++ b/ReactiveUI/ObservableAsPropertyHelper.cs @@ -141,7 +141,7 @@ static ObservableAsPropertyHelper observableToProperty( Expression> property, TRet initialValue = default(TRet), IScheduler scheduler = null) - where TObj : ReactiveObject + where TObj : IReactiveObject { Contract.Requires(This != null); Contract.Requires(observable != null); @@ -182,7 +182,7 @@ public static ObservableAsPropertyHelper ToProperty( Expression> property, TRet initialValue = default(TRet), IScheduler scheduler = null) - where TObj : ReactiveObject + where TObj : IReactiveObject { return source.observableToProperty(This, property, initialValue, scheduler); } @@ -208,7 +208,7 @@ public static ObservableAsPropertyHelper ToProperty( out ObservableAsPropertyHelper result, TRet initialValue = default(TRet), IScheduler scheduler = null) - where TObj : ReactiveObject + where TObj : IReactiveObject { var ret = source.observableToProperty(This, property, initialValue, scheduler); diff --git a/ReactiveUI/Xaml/ViewModelViewHost.cs b/ReactiveUI/Xaml/ViewModelViewHost.cs index 84eb95d5c8..e9c7267daa 100644 --- a/ReactiveUI/Xaml/ViewModelViewHost.cs +++ b/ReactiveUI/Xaml/ViewModelViewHost.cs @@ -50,6 +50,14 @@ public IObservable ViewContractObservable { public static readonly DependencyProperty ViewContractObservableProperty = DependencyProperty.Register("ViewContractObservable", typeof(IObservable), typeof(ViewModelViewHost), new PropertyMetadata(Observable.Return(default(string)))); + private string viewContract; + + public string ViewContract + { + get { return this.viewContract; } + set { ViewContractObservable = Observable.Return(value); } + } + public IViewLocator ViewLocator { get; set; } public ViewModelViewHost() @@ -103,6 +111,11 @@ public ViewModelViewHost() Content = view; })); }); + + this + .WhenAnyObservable(x => x.ViewContractObservable) + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(x => this.viewContract = x); } static void somethingChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)