Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

Already on GitHub? Sign in to your account

Create Generic ReactiveUserControl<ViewModel> #348

Closed
bradphelan opened this Issue Jul 17, 2013 · 3 comments

Comments

Projects
None yet
3 participants

Even though generic view classes are a bit of a pain in WPF they can be done and this class adds some very nice behaviors.

using ReactiveUI.Ext;
using System;
using System.Windows;
using System.Windows.Controls;
using System.Reactive.Linq;
using System.Windows.Media;
using System.Reactive.Disposables;
using System.Windows.Markup;
using System.Windows.Controls.Primitives;

namespace ReactiveUI
{
    [ContentProperty("AdditionalContent")]
    public class ReactiveUserControl<ViewModelT> : UserControl, IViewFor<ViewModelT>
        where ViewModelT : class 
    {
        #region IViewFor<ViewModelT>
        public static readonly DependencyProperty ViewModelProperty =
            DependencyProperty.Register("ViewModel", typeof(ViewModelT), typeof(ReactiveUserControl<ViewModelT>), new PropertyMetadata(null));

        public ViewModelT ViewModel
        {
            get { return (ViewModelT)GetValue(ViewModelProperty); }
            set { SetValue(ViewModelProperty, value); }
        }

        object IViewFor.ViewModel
        {
            get { return (ViewModelT)GetValue(ViewModelProperty); }
            set { SetValue(ViewModelProperty, value); }
        }
        #endregion

        /// <summary>
        /// Gets or sets additional content for the UserControl. The additional
        /// content will have it's DataContext set to an instance of the ViewModel
        /// </summary>
        public UIElement AdditionalContent
        {
            get { return (UIElement)GetValue(AdditionalContentProperty); }
            set { SetValue(AdditionalContentProperty, value); }
        }

        public static readonly DependencyProperty AdditionalContentProperty =
            DependencyProperty.Register("AdditionalContent", typeof(UIElement), typeof(ReactiveUserControl<ViewModelT>),
              new PropertyMetadata(null));

        public IObservable<ViewModelT> ViewModelObservable()
        {
            return this.WhenAny(x => x.ViewModel, x => x.Value)
                .Where(v=>v!=null);
        }

        public UniformGrid DataContextHost { get; private set; }

        public ReactiveUserControl()
        {
            DataContextHost = new UniformGrid();
            this.Content = DataContextHost;

            // If AdditionalContent changes then we need to
            // add it to the visual tree
            this.WhenAny(p => p.AdditionalContent, p => p.Value)
                .Where(v=>v!=null)
                .Subscribe(v => {
                    DataContextHost.Children.Clear();
                    DataContextHost.Children.Add(v);
                });

            // If the ViewModel changes we need to ensure the
            // AdditionalContent get's the correct DataContext
            this.WhenAny(x => x.ViewModel, x => x.Value)
                .BindTo(this, x=>x.DataContextHost.DataContext);

            DesignUnsafeConstruct();
            if (!System.ComponentModel.DesignerProperties.GetIsInDesignMode(this))
            {

                DesignSafeConstruct();

            }
        }

        public virtual void DesignUnsafeConstruct(){
        }

        public virtual void DesignSafeConstruct(){
        }
    }

    public static class ReactiveUserControlMixins
    {
        // Dispose the previous disposable when the next is registered
        // and then dispose the last one when the UI is shut down
        public static void SeriallyDisposeWith(this IObservable<IDisposable> This, Control control)
        {
            var disposer = new SerialDisposable();
            This.Subscribe(d=>disposer.Disposable=d);
            control.Unloaded += (s, e) => disposer.Dispose();
            control.Dispatcher.ShutdownStarted += (s, e) => disposer.Dispose();
        }

        // Dispose when unloaded or dispatcher is shut down
        public static void DisposeWith(this IDisposable This,  Control contrl)
        {
            contrl.Unloaded += (s, e) => This.Dispose();
            contrl.Dispatcher.ShutdownStarted += (s, e) => This.Dispose();
        }
    }

}

Usage is something like

namespace WeinCad.Controls.View
{
    public class MoineauMillingViewBase : ReactiveUserControl<MoineauMillingViewModel> { }
    /// <summary>
    /// Interaction logic for MoineauMillingView.xaml
    /// </summary>
    public partial class MoineauMillingView : MoineauMillingViewBase
    {
        public MoineauMillingView()
        {
            InitializeComponent();
        }
    }
}

and in XAML

<l:MoineauMillingViewBase x:Class="WeinCad.Controls.View.MoineauMillingView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:l="clr-namespace:WeinCad.Controls.View"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <Grid>
        <Label>Moin Moin Milling</Label>

    </Grid>
</l:MoineauMillingViewBase>

The reactiveuser control wraps internal content in a hidden control on which the datacontext is set to the ViewModel. This automatically protects the internal DataContext from being set if a user directly sets the DataContext on an instance of the control.

There are also some helpers for managing IDisposable lifetimes in terms of load and unload events.

It's not a pull request as I have this code internal to my codebase but if you like I'll take the effort to make a patch

Contributor

Haacked commented Jul 17, 2013

Geez! You're taking all my good ideas right out of my brain! I had thought of doing this too but have been too busy. I ❤️ it!

I wonder if we should add some of the common Bind methods too. That way you don't have to do the awkward this.Bind(...) calls.

@bradphelan bradphelan added a commit to bradphelan/ReactiveUI that referenced this issue Jul 18, 2013

@bradphelan bradphelan Fixes #348 358bffb

@Haacked I've created a pull request so you can try out the branch if you like. The feature I'd really like is a template where I can click New ReactiveUserControl and all the nasty XAML boilerplate and namespace crap is filled in for me. That would be a nice addition if anybody has experience with that stuff.

Member

jlaanstra commented Apr 27, 2014

Tracked in #349.

@jlaanstra jlaanstra closed this Apr 27, 2014

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment