Create a New View Inside Another View's ViewModel

Topics: Prism v2 - WPF 3.5
Jan 10, 2010 at 7:53 PM

A view command handler in my ViewModel needs to create a new View that is displayed in a popup region.  It seems to me that a ViewModel creating Views violated the MVVM triad.  What would be the best-practices way of doing this?  Should I register my popup View and then use an even to show it?

Jan 11, 2010 at 5:30 PM

I am currently doing this exact same thing.
For example... I have three regions in my application: ApplicationRibbonRegion, ApplicationMainRegion, and ApplicationStatusBarRegion.

When I activate/show a View in the ApplicationMainRegion I need to show some correspoding Views in the other two regions.
I am uncertain as to how others have accomplished this but for the most part my Views have a one-to-one relationship with their ViewModel.
The ViewModel has a reference to the View and hooks into the View's Loaded and Unloaded events. I use these two events to add/remove corresponding Views to other regions if needed.

If someone has a better way of handling this particular problem please share it as I have struggled to find a better way.

Michael

Jan 12, 2010 at 3:32 AM

My ViewModel has no reference whatsoever to the View.  The ViewModel is injected into the View using the Container when its registered with a given region.  If I need to communicate back to the View from the ViewModel, I raise an event that the View has a handler for.  I am not using the EventAggregator between the View and ViewModel.  I only use the EventAggregator between ViewModels or between Services, or some combination of both.

My other issue is my model is a library unto itself.  It is however, not a module, but it does expose services.  How do other folks handle their model?  Can a model have its own Views also?

Jan 12, 2010 at 3:50 AM

How are you setting the DataContext (ViewModel) for the View that the ViewModel represents?

Jan 12, 2010 at 4:12 AM

I'm doing this from memory because I'm not in the office, so bear with me.  

namespace foo {

  class View {

    private readonly ViewModel _viewModel;

    public View(){
      InitializeComponents();
    }

    public View(ViewModel viewModel) : this() {
      _viewModel = viewModel;
      DataContext = _viewModel;
      _viewModel.RequestClose += OnRequestClose;
    }

    private void OnRequestClose(object sender, EventArgs e){
      // do some stuff :)
    }

  }

}

The key here is that the instantiation of the View must be handled by the Container.  For example, if you call RegisterViewWithRegion, you will pass a Type of your View.  The Container will instantiate it and inject the ViewModel into the View.  Let me know if you have any other questions and I'll answer them tomorrow when I have my codebase in front of me.

Jan 12, 2010 at 4:27 AM

Instead of using RegisterViewWithRegion... you might think about using Region.Add().
This how I do it and so far it works well (if you are not opposed to your ViewModels knowing about their Views use the code below)
You can override the OnViewLoaded to add/activate Views in another region if needed and override OnViewUnloaded to remove/deactivate Views in that same region.

public abstract class ViewModel : ObservableObject
{

	#region [ Fields ]

	private readonly Dispatcher _dispatcher;

	#endregion

	#region [ Constructors ]

	protected ViewModel()
	{
		this._dispatcher = Application.Current.Dispatcher;
	}

	#endregion

	#region [ Properties ]

	public Dispatcher Dispatcher
	{
		get
		{
			return this._dispatcher;
		}
	}

	#endregion

	#region [ Methods ]

	public void CheckAccessInvokeAction(Action action)
	{
		if (null == action)
			throw new ArgumentNullException("Action cannot be null");

		if (this.Dispatcher.CheckAccess() == false)
		{
			this.Dispatcher.BeginInvoke(new ThreadStart(delegate { this.CheckAccessInvokeAction(action); }), DispatcherPriority.Send);
		}
		else
		{
			action.Invoke();
		}
	}

	#endregion

}

public abstract class ViewModel<TView> : ViewModel
	where TView : FrameworkElement, IView
{
	#region [ Fields ]

	private TView _view;

	#endregion

	#region [ Constructors ]

	protected ViewModel(TView view) : base()
	{
		this.View = view;
	}

	#endregion

	#region [ Properties ]

	protected TView View
	{
		get
		{
			return this._view;
		}
		set
		{
			if (value != this._view)
			{
				this.OnViewChanging();
				this._view = value;
				this.OnViewChanged();
			}
		}
	}

	#endregion

	#region [ Methods ]

	protected virtual void OnViewChanging()
	{
		if (null != this.View)
		{
			this.View.Loaded -= new RoutedEventHandler(this.View_Loaded);
			this.View.Unloaded -= new RoutedEventHandler(this.View_Unloaded);
		}
	}

	protected virtual void OnViewChanged()
	{
		if (null != this.View)
		{
			this.View.Loaded += new RoutedEventHandler(this.View_Loaded);
			this.View.Unloaded += new RoutedEventHandler(this.View_Unloaded);
		}
	}

	protected virtual void OnViewLoaded()
	{

	}

	protected virtual void OnViewUnloaded()
	{

	}

	#endregion

	#region [ Event Handlers ]

	private void View_Loaded(object sender, RoutedEventArgs e)
	{
		this.OnViewLoaded();
	}

	private void View_Unloaded(object sender, RoutedEventArgs e)
	{
		this.OnViewUnloaded();
	}

	#endregion
}
Jan 12, 2010 at 3:07 PM

Yes, I have an enormous problem with my ViewModel being aware of the View.  The whole idea about seperation is that, theoretically, I could use my ViewModel with any sort of View.  Yes, I have used the Add() method.  The only time I do that is if I have to pass a value to the ViewModel prior to instantiating the View.  I do it as such:

            _viewModel = _locator.GetInstance<ViewModel>();
            _viewModel.SomeProperty = foo;
            _view = _locator.GetInstance<View>();
            _view.DataContext = _viewModel;

            var region = _regionManager.Regions[RegionNames.SecondaryRegion];
            region.Add(_view);
            region.Activate(_view);


 

Jan 12, 2010 at 3:57 PM

I tried your approach to begin with keeping my ViewModels ignorant of the Views... and in some circumstances I still do this.
However, this approach always gave me grief. I very RARELY interact directly with the View from the ViewModel... mainly it is there so that I can hook into the Loaded and Unloaded events.

Using your example from above.... when constructing the View catch the Loaded and Unloaded events on the View and then call a specific method on the ViewModel.

namespace foo {

  class View {

    private readonly ViewModel _viewModel;

    public View(){
      InitializeComponents();
    }

    public View(ViewModel viewModel) : this() {
      _viewModel = viewModel;
      DataContext = _viewModel;
      _viewModel.RequestClose += OnRequestClose;
      this.Loaded += new RoutedEventHandler(this.View_Loaded);
    }

    private void OnRequestClose(object sender, EventArgs e){
      // do some stuff :)
    }

    private void View_Loaded(object sender, RoutedEventArgs e)
    {
       this._viewModel.OnViewLoaded();
    }

  }

}

Jan 12, 2010 at 4:18 PM

You're absolutely right.  As I said, I was doing it from memory.  Yes, I hook the Loaded event (i just wire it up in the XAML) and then call a Start() method on my ViewModel.  The Start() method does things such as pulling data from the DB and populating bound properties (subsequently raising the property changed event).

 

Jan 12, 2010 at 4:20 PM

I would hook in to the Unloaded event as well and just activate any views you need to add to other regions in the Loaded event and then deactivate them in the Unloaded event.