59

I have a Button and I bind this button to a command in ViewModel say OpenWindowCommand. When I click on the button I want to open a new window. But creating a window instance and showing a window from view model is a violation of MVVM. I have created interface like

interface IWindowService
{
    void showWindow(object dataContext);
}

and WindowService implements this interface like

class WindowService : IWindowService
{
    public void showWindow(object dataContext)
    {
        ChildWindow window=new ChildWindow();
        window.DataContext=dataContext;
        window.Show();
    }
}

In this class I have specified ChildWindow. So this class is tightly coupled with showing ChildWindow. When I want to show another window, I have to implement another class with the same interface and logic. How can I make this class generic so that I can just pass an instance of any window and the class will be able to open any window?

I am not using any built MVVM frameworks. I have read many articles on StackOverflow but I could not found any solution for this.

prelude38
  • 107
  • 7
DT sawant
  • 1,201
  • 2
  • 10
  • 21
  • 2
    I've found an [alternative way](http://stackoverflow.com/a/15512972/385995) of opening windows in MVVM, using a behavior instead of a service. – Mike Fuchs Sep 19 '14 at 14:28

7 Answers7

63

You say "creating window instance and showing window from view model is violation of MVVM". This is correct.

You are now trying to create an interface that takes a type of view specified by the VM. This is just as much of a violation. You may have abstracted away the creation logic behind an interface, but you are still requesting view creations from within the VM.

VM's should only care about creating VM's. If you really need a new window to host the new VM, then provide an interface as you have done, but one that does NOT take a view. Why do you need the view? Most (VM first) MVVM projects use implicit datatemplates to associate a view with a particular VM. The VM knows nothing about them.

Like this:

class WindowService:IWindowService
{
    public void ShowWindow(object viewModel)
    {
        var win = new Window();
        win.Content = viewModel;
        win.Show();
    }
}

Obviously you need to make sure you have your VM->View implicit templates set up in app.xaml for this to work. This is just standard VM first MVVM.

eg:

<Application x:Class="My.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:vm="clr-namespace:My.App.ViewModels"
             xmlns:vw="clr-namespace:My.App.Views"
             StartupUri="MainWindow.xaml">
    <Application.Resources>

        <DataTemplate DataType="{x:Type vm:MyVM}">
            <vw:MyView/>
        </DataTemplate>

    </Application.Resources>
</Application>
GazTheDestroyer
  • 20,722
  • 9
  • 70
  • 103
  • How can I open a window without passing window to function? – DT sawant Sep 15 '14 at 10:50
  • 3
    Why do you need different Window types? The window is just a container for the view. Just use a generic window and use implicit DataTemplates as normal for mapping VM->View – GazTheDestroyer Sep 15 '14 at 10:52
  • Do you mean I should use only one window and I do not have to open new windows? – DT sawant Sep 15 '14 at 10:54
  • Well ideally yes, but that's another discussion. What I actually meant was just use one _type_ of Window and let the views map themselves as normal. – GazTheDestroyer Sep 15 '14 at 10:57
  • But What is solution for opening totally new window when it's project requirement.and parent viewmodel needs to host this window. – DT sawant Sep 15 '14 at 11:00
  • 3
    One of the reasons the VM knows nothing about the View is because you can have multiple Views to display the data in the ViewModel in different ways. This method makes your views and viewmodels have a 1:1 mapping. – Nick Mar 15 '15 at 16:03
  • @Nick. No, it only provides a default mapping. There are plenty of easy ways to throw an explicit different view into the ContentControl (eg triggers). In my experience it's fairly rare anyway. – GazTheDestroyer Oct 15 '15 at 13:25
  • 12
    This solution will not working if MyView is a Window. It will throw an error of 'Can't put window in style' – Jack Frost Mar 31 '16 at 06:40
  • Where should I need to have `WindowService`? I hope it should be in view then I can I invoke it from VM? Any ideas? – Gopichandar Jul 18 '16 at 07:02
  • 10
    I hope `vw:MyView` should be of type `UserControl` and not of type `Window` – Gopichandar Aug 24 '16 at 10:23
  • 2
    I know this is late comment, but I agree with @Gopichandar because when i add Window into a Content, it throws a runtime exception "Window must be the root of the tree. Cannot add Window as a child of Visual" – Sats Nov 18 '18 at 10:14
  • Do you know why it underlines DataType="{x:Type vm:MyVM}"> for me? What am I missing? – Ehsan Jan 09 '20 at 16:01
  • Can anybody say where this namespace should be set? xmlns:vw="clr-namespace:My.App.Views" – Ehsan Jan 29 '20 at 18:43
  • 1
    @Ehsan Wherever your view (ie xaml) is defined. – GazTheDestroyer Jan 31 '20 at 09:43
  • Thanks for replying. Do you mean that I need to put this in both application XAML and all views that are considered under this view namespace? – Ehsan Feb 04 '20 at 15:00
  • @Ehsan It just needs to go in app.xaml. The namespace just points to where your views are defined. If you have views in multiple namespaces then you will need to define multiple namespaces in app.xaml to point to them – GazTheDestroyer Feb 05 '20 at 09:08
  • This answer should be updated -"**new Window()**" is misleading. "**Window**" should be actual Window name, not just type. – LuckyLuke82 Sep 29 '20 at 07:29
  • @LuckyLuke82 No, the whole point is that it just uses a generic window to host the view. – GazTheDestroyer Sep 29 '20 at 08:16
  • @GazTheDestroyer, how, what is generic Window ? I copied exact code, set DataTemplate in App.xaml and got error: "**Window must be the root of the tree. Cannot add Window as a child of Visual.**'". I tried to open new Window from ViewModel, like this: **_windowservice.ShowWindow(new MyViewModel());**. – LuckyLuke82 Sep 29 '20 at 09:31
  • 2
    @LuckyLuke82 It sounds like you're defining your view as a Window. It should be a UserControl. – GazTheDestroyer Sep 29 '20 at 10:21
  • @GazTheDestroyer, now I get you. Yes, my View is a Window, that is why I said answer is wrong. It should be updated - either explicitly say that **win.Content** should be a UserControl which will be displayed in this Window (and set via DataTemplate in app.xaml), or **new Window()** should be a specific View name e.g. **MyWindow1()**. This is not mentioned in answer, neither in question, so It's misleading. Thanks for clarifying though, with UserControl is this solution perfectly fine. – LuckyLuke82 Sep 29 '20 at 11:33
  • I edited this to mention the UserControl requirement. Also, instead of object, if you use a base class for your view models, better to use that. Also, you should set the window to size itself to content, or set an explicit size. – Jamie Sep 04 '21 at 03:53
  • While this solution really helped me, it didn't work or make sense because my DataTemplate in App.xml was not being recognised. Once I solved that optimisation bug (https://stackoverflow.com/a/4813660/3833950) this solutoon worked like magic. – bbarrett Mar 15 '22 at 02:09
  • How do we set different `Window` properties for different ViewModels when using this convention, i.e. `Window` should be resizable when `ViewModelA` is applied but not when `ViewModelB` is applied etc.? – AntikM Jun 16 '23 at 07:56
  • @antikbd Presumably just use a custom window and bind `ResizeMode` to the VM? – GazTheDestroyer Jun 16 '23 at 08:14
  • @GazTheDestroyer I thought of that approach but wouldn't that be projecting the View's properties into the VM (and thus violating MVVM)? The View can be a console window for all VM cares. Different Views (consoles, windows, HTML pages etc.) can have many different properties of their own and trying to define them all in the VM would be tedious too. Ideally the View should change its own properties based on the applied VM and not the other way round, right? – AntikM Jun 16 '23 at 08:24
  • @antikbd Sometimes you just have to be pragmatic. It's not like the VM is pulling in any view related classes, it's just setting a flag that the view/window can honour. However if you really feel strongly about it then you can always have the view do the setting of the Window property, or have the Window bind to a DP on the view rather than the VM – GazTheDestroyer Jun 16 '23 at 08:38
7

One possibility is to have this:

class WindowService:IWindowService
{
 public void showWindow<T>(object DataContext) where T: Window, new() 
 {
  ChildWindow window=new T();
  window.Datacontext=DataContext;
  window.show();
 }
}

Then you can just go something like:

windowService.showWindow<Window3>(windowThreeDataContext);

For more information on the new constraint, see http://msdn.microsoft.com/en-gb/library/sd2w2ew5.aspx

Note: the new() constraint only works where the window will have a parameterless constructor (but I imagine this shouldn't be a problem in this case!) In a more general situation, see Create instance of generic type? for possibilities.

Community
  • 1
  • 1
David E
  • 1,384
  • 9
  • 14
  • 11
    windowService.showWindow(windowThreeDataContext); this statement is in viewmodel and it contains name of view.Doesn't it violet the MVVM approach? – DT sawant Sep 15 '14 at 10:52
  • Indeed - sorry, I was taking the question `How can I make this class generic so that I can pass just instance of any window and class will be able to open any window?` a little too strongly, and didn't properly discuss the root issue! Somewhere in an MVVM approach you will need to make windows/views, so the above can be useful - potentially you either have a mapping from ViewModel to View, or some form of convention (eg `\ViewModels\MyViewModel.cs -> \Views\MyView.cs`), but it's up to you :) – David E Sep 15 '14 at 11:22
  • 1
    Personally, I'd advocate using a framework if you want to go for a hardcore MVVM approach, which usually wraps all this up for you :). I've used `Caliburn Micro` which I really like, but it's up to you ^^ – David E Sep 15 '14 at 11:23
4

You could write a function like this:

class ViewManager
{
    void ShowView<T>(ViewModelBase viewModel)
        where T : ViewBase, new()
    {
        T view = new T();
        view.DataContext = viewModel;
        view.Show(); // or something similar
    }
}

abstract class ViewModelBase
{
    public void ShowView(string viewName, object viewModel)
    {
        MessageBus.Post(
            new Message 
            {
                Action = "ShowView",
                ViewName = viewName,
                ViewModel = viewModel 
            });
    }
}

Make sure the ViewBase has a DataContext property. (You could inherit UserControl)

In general I would make some kind of message bus and have a ViewManager listen for messages asking for a view. ViewModels would send a message asking for a view to be shown and the data to show. The ViewManager would then use the code above.

To prevent the calling ViewModel to know about the View types you could pass a string/logical name of the view to the ViewManager and have the ViewManager translate the logical name into a type.

Emond
  • 50,210
  • 11
  • 84
  • 115
  • I may be wrong, but I'm fairly sure you need the `where T: ViewBase, new()` in order to create a new object of generic type in your function? a la: http://msdn.microsoft.com/en-gb/library/sd2w2ew5.aspx – David E Sep 15 '14 at 10:30
  • It was a case of simultaneous answering - yours wasn't there when I wrote mine, then I refreshed to find you had beaten me to it! Apologies Erno :) – David E Sep 15 '14 at 10:39
  • @DavidEdey - :) np it is not a race – Emond Sep 15 '14 at 10:41
  • 1
    @ErnodeWeerd In your case I will still need to refer view from viewmodel. I don't have to create instance there but at least I have to refer to View.So doesn't it violet MVVM? – DT sawant Sep 15 '14 at 11:14
  • @DTsawant - no you don't. Read the last bit of my answer. – Emond Sep 15 '14 at 11:22
  • I also added the sending of a message. Beware though, if you continue this way, you'll end up writing an MVVM library that already exists :) – Emond Sep 15 '14 at 11:29
3

use a contentpresenter in your Window where you bind your DataConext to. And then define a Datatemplate for your DataContext so wpf can render your DataContext. something similar to my DialogWindow Service

so all you need is your one ChildWindow with a ContentPresenter:

<Window x:Class="ChildWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
WindowStartupLocation="CenterOwner" SizeToContent="WidthAndHeight">
<ContentPresenter Content="{Binding .}">

</ContentPresenter>
</Window>
Community
  • 1
  • 1
blindmeis
  • 22,175
  • 7
  • 55
  • 74
2

I find the accepted solution very useful, but when trying it practically, I found that it lacks the ability to make the UserControl (the View that results from the VM -> View mapping) dock within the hosting window in order to occupy the whole area provided by it. So I extended the solution to include this ability:

public Window CreateWindowHostingViewModel(object viewModel, bool sizeToContent)
{
   ContentControl contentUI = new ContentControl();
   contentUI.Content = viewModel;
   DockPanel dockPanel = new DockPanel();
   dockPanel.Children.Add(contentUI);
   Window hostWindow = new Window();
   hostWindow.Content = dockPanel;

   if (sizeToContent)
       hostWindow.SizeToContent = SizeToContent.WidthAndHeight;

   return hostWindow;
}

The trick here is using a DockPanel to host the view converted from the VM.

Then you use the previous method as follows, if you want the size of the window to match the size of its contents:

var win = CreateWindowHostingViewModel(true, viewModel)
win.Title = "Window Title";
win.Show();

or as follows if you have a fixed size for the window:

var win = CreateWindowHostingViewModel(false, viewModel)
win.Title = "Window Title";
win.Width = 500;
win.Height = 300;
win.Show();
0

Maybe you could pass the window type.

Try using Activator.CreateInstance().

See the following question: Instantiate an object with a runtime-determined type.

Solution by chakrit:

// determine type here
var type = typeof(MyClass);

// create an object of the type
var obj = (MyClass)Activator.CreateInstance(type);
Community
  • 1
  • 1
Hellin
  • 62
  • 8
0

Here it is a little contribution. In order to be more generic we can get the type of viewModel that is being passed to the method showWindow, then look for its corresponding view and finally instantiate it.

class WindowService : IWindowService
    {
        public void showWindow(object viewModel)
        {
            string viewToSearch;
            Type foundViewType;


            // Find the type of the viewModel
            Type t = viewModel.GetType();

            // Get the views            
            List<Type> myViews = Assembly.GetExecutingAssembly().GetTypes()
                      .Where(t => t.Namespace == "[yourNameSpace].Views")
                      .ToList();

            // Find the corresponding view
            viewToSearch = t.Name.Replace("ViewModel", "View");
            foundViewType = myViews.Find(x => x.Name.Equals(viewToSearch));

            if (foundViewType != null)
            {
                var window = Activator.CreateInstance(foundViewType);

                ((Window)window).DataContext = viewModel;
                ((Window)window).Show();
            }

        }
    }

Then you can call it in your viewModel

WindowService windowService = new WindowService();
windowService.showWindow(this);