0

I am new to WPF and the programming world in general and I need help. I am building a WPF MVVM application.

I want to create a method, which takes a View (or the name of a View) and returns its ViewModel. I need it to use an instance of the ViewModel in the MainViewModel (or ShellViewModel) without creating one with = new() everytime. I just want to put the name of the View as an argument and for it to return the ViewModel.

I was thinking of using ViewModelLocator from https://www.c-sharpcorner.com/article/datacontext-autowire-in-wpf/ and making it work for my case, but perhaps maybe there is a better way to do this.

Another idea is to add all the ViewModels in a class, e.g ViewModelCollector, and implement a GetViewModel method and call it in MainViewModel each time I need another ViewModel, but it's just an idea I'm not sure how to implement yet.

What is the best approach here?

What I have:

MainView.xaml:

<Grid>
    <ItemsControl ItemsSource="{Binding NavigationItems, UpdateSourceTrigger=PropertyChanged}">
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <RadioButton GroupName="Menu" IsChecked="{Binding IsActive, UpdateSourceTrigger=PropertyChanged}"
                            Command="{Binding DataContext.ShowPageCommand, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type UserControl}} }" 
                            CommandParameter="{Binding NavigationIndex}">
                    <RadioButton.Style>
                        //some style
                    </RadioButton.Style>
                    <RadioButton.Content>
                        <Grid  Margin="25,10">
                            <TextBlock Grid.Row="1" Text="{Binding Name}" />
                        </Grid>
                    </RadioButton.Content>
                </RadioButton>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>
</Grid>

MainView.xaml.cs:

public MainView()
{
      InitializeComponent();
      DataContext = new MainViewModel();
}

(MainViewModel should be injected here as far as I know but it currently doesn't work that way)

I have 2 other ViewModels, let's say FirstViewModel and SecondViewModel. Services are injected in the constructors of both ViewModels.

Every ViewModel (except MainViewModel) implements INavigation:

string Name { get; set; }
string Source { get; set; }
int NavigationIndex { get; set; }
bool IsActive { get; set; }

And there is a ConfigureView method in every constructor:

public FirstViewModel(SomeService service)
{
    this.service = service ?? throw new ArgumentNullException(nameof(service));

    //not important things

    ConfigureView();
}

private void ConfigureView()
{
    Name = "First";
    Source = "FirstView.xaml";
}

MainViewModel.cs:

public ObservableCollection<INavigation> NavigationItems { get; set; } //it is set in a different way, using INotifyPropertyChanged

public INavigation SelectedNavigation { get; set; } //it is set in a different way, using INotifyPropertyChanged

public ICommand ShowPageCommand { get; set; }

public MainViewModel()
{
    ShowPageCommand = new RelayCommand<int>(ShowPage);

    int index = 0;
    NavigationItems = new ObservableCollection<INavigation>();
    NavigationItems.Add(new FirstViewModel { NavigationIndex = index++, IsActive = true }); //error
    NavigationItems.Add(new SecondViewModel { NavigationIndex = index++ }); //error

    SelectedNavigation = NavigationItems[0];
}

private void ShowPage(int index)
{
    SelectedNavigation = NavigationItems[index];
}

The error is that there is no default constructor. How can I add the ViewModels to NavigationItems without creating an instance of them in MainViewModel?

Melissa
  • 135
  • 1
  • 7
  • 1
    The view model should be accessible by the view's DataContext property. – Clemens Oct 20 '21 at 06:55
  • I haven't seen your code, but there I believe you are doing it in a way which contradicts the ideas behind MVVM. You want the ViewModel belonging to another View, and you want to call this from your MainViewModel? Your MainViewModel shouldn't know about your View. What do you actually want to achieve? – SomeBody Oct 20 '21 at 06:56
  • I am aware that the ViewModel shouldn't know about the View but I am implementing my own navigation between pages (I have a menu in my MainWindow). The ViewModel has a method ConfigureView() which sets the View. This method is called in the ViewModel's constructor. – Melissa Oct 20 '21 at 07:01
  • Also, the MainViewModel keeps an ObservableCollection - NavigationItems - which holds all the ViewModels and a SelectedNavigation which holds the thing that has been selected. INavigation is an interface which consists of Name, Source (the name of the view) and etc. Every ViewModel from the menu implements it. – Melissa Oct 20 '21 at 07:06
  • You could use a content control and bind its content to the SelectedNavigation. You can tell in XAML which viewmodel uses which view - see e.g. this question: https://stackoverflow.com/questions/11570754/binding-viewmodel-to-contentcontrol-as-its-datacontext/11571028 – SomeBody Oct 20 '21 at 07:11
  • Does this answer your question? [C# WPF Navigation Between Pages (Views)](https://stackoverflow.com/questions/61320671/c-sharp-wpf-navigation-between-pages-views) – BionicCode Oct 20 '21 at 08:20
  • @BionicCode At first I thought it did, yes, but as far as I know instantiating new objects in the constructor is bad practice (I could be wrong) and in this link in `MainViewModel` the other `ViewModel`s are created using `= new WelcomePageViewModel()` and I want to avoid that. I also inject dependencies in the constructor of the other ViewModels so it won't work in my case. – Melissa Oct 20 '21 at 09:09
  • @Melissa Next time, I recommend that you show us some of your code. You will get much better answers and we can directly tell you what advantages and disadvantages your approach has. – SomeBody Oct 20 '21 at 10:20
  • I will update the question in a minute. – Melissa Oct 20 '21 at 10:23
  • @Mellisa concerning the injection of services, I had similar issues with using constructor injection in my viewmodels. I used the service locator pattern to inject my dependencies and it worked. I don't know if it may help with your situation. – Jeffery Oct 20 '21 at 11:19
  • @Jeffery I also thought about that! I read somewhere that it is an anti-pattern and it breaks SOLID and Encapsulation and I'm not sure if it is the right approach. But if it worked in your case, I might try to implement it here, thank you! – Melissa Oct 20 '21 at 11:21
  • Instantiating objects in a constructor is not bad practice at all. Don't know how came to this conclusion. If you want to improve extensibility of your application you may choos to implement a IoC pattern like Dependency Injection. You wrote that you already use constructor injection - then why don't you inject the view model instances too? Where is the problem? You can also opt to inject a factory if you need to give the MainVieModel more control over the instantiation. – BionicCode Oct 20 '21 at 13:31
  • *"I want to create a method, which takes a View (or the name of a View) and returns its ViewModel"*. The provided link shows you how to implement page navigation using the view-model-first pattern. You should let WPF create the view for you using DataTemplates. You should not pass around views to select a corresponding view model class. THAT'S a bad idea. You can modify the example to use constructor injection. I don't really see the problem to modify the example to meet your needs in regards of type instantiation. I think the provided link does solve your exact problem. – BionicCode Oct 20 '21 at 13:37
  • Use the DataTemplate approach from the link and forget about any Locator pattern or looking up model classes by view! – BionicCode Oct 20 '21 at 13:41
  • @BionicCode I am using this approach right now, but when I am adding the views in a DataTemplate in Window.Resources I get an error `Type (of the view) is not usable as an object element because it is not public or does not define a public parameterless constructor or a type converter`. The constructor in the view injects the ViewModel and sets the DataContext to the ViewModel. And the ViewModel's constructor also has parameters, so I can't type `DataContext = new FirstViewModel()` => the ViewModel has to be injected, if it is set from code-behind. Do you know any solution to that? – Melissa Oct 20 '21 at 16:29
  • I think you misunderstood some critical parts. View-Model-First: you first create the instance of the data model (it doesn't matter where you create it). You add this data model to the view using control that allows to template the content (data model) like a ContentControl. You create a DataTemplate that targets the data model's type. There is no need to have a parameterless constructor as the data model is already instantiated => View-Model-First. The DataTemplate is then applied to render the view which then has the data model automatically set as its DataContext. – BionicCode Oct 20 '21 at 19:44
  • What ever you have done that has triggered the error message, you did it wrong. I appears that for some reason you were trying to create instances of tzhe data model in your XAML code. I can only repeat myself: follow the example of the link I have given you. Just replace the instantiation with constructor "injection" (aggregation) - that's it and you are done. The example shows how to define a DataTemplate properly. It also shows how bind the data models to a ContentControl which will apply the DataTemplate. It's all there. – BionicCode Oct 20 '21 at 19:45
  • Your problems are related to your misunderstanding and for some reason you are too focused on type instantiation, so you don't get the picture. Type instantiation is not a problem at all. It has nothing to do with the way you show your views/pages. It's a totally different problem. – BionicCode Oct 20 '21 at 19:45
  • In case you have created a control that you want to display using a DataTemplate, then yes, the control must have a parameterless constructor. But this is not a problem as the DataContext of this control is automatically set (it inherits the DataContext of the DataTemplate - which is the data model the template is applied to). You should not create the DataContext explicitly. – BionicCode Oct 20 '21 at 19:48
  • Read through your last comment again and it seems that your misunderstanding of the way how DataContext and DataTemplate behave is the true nature of your confusion. Don't create the DataContext explicitly. It will be inherited by the view from the DataTemplate. Just follow the example I have posted. It does all the things right that you are curtrently doing wrong... – BionicCode Oct 20 '21 at 19:52
  • You must also differentiate between a view that is statically shown like the MainWindow and views that are dynamically shown (via DataTemplate). You don't configure such views for dependency injection (as their dependency is inherited as DataContext from the DataTemplate). When you talk about injection, does this mean you are using some IoC framework? Generally, there is no such rule or best practice that you have to "inject" the DataContext if you want to set it from the code-behind. Dependency injection (if this is what you mean by "inject") is based on totally different considerations. – BionicCode Oct 20 '21 at 20:04
  • 1
    @BionicCode Thank you for all the effort! I think I am starting to get there! I did everything from the link you posted. I am no longer setting the DataContext from the View code-behind and I am letting the DataTemplate handle it. I am using Dependency Injection, yes. I have a quick last question -> `Just replace the instantiation with constructor "injection" (aggregation)` are you talking about MainViewModel here? Inject the other ViewModels in the constructor of the MainViewModel and write - `Pages = new Dictionary{ PageName.FirstPage, firstViewModel }` ? – Melissa Oct 21 '21 at 06:23
  • @Melissa Exactly. You must inject the page view models into the class that hosts the page models i.e. that has the dependency. Also note that in case you want to create a new page each time you switch to a new page, you must inject factories rather than the actual instances (this means pages will lose their state). If you want your pages to "remember" their state between switches then (re)use a single injected instance. – BionicCode Oct 21 '21 at 10:32
  • @BionicCode Thanks! I did it this way and now it won't let me set the DataContext of MainView to MainViewModel from the xaml because there is no parameterless constructor for MainViewModel. I tried setting it in code-behind by injecting MainViewModel and now it compiles, but it throws a NullReferenceException somewhere... – Melissa Oct 21 '21 at 10:39
  • You have to compose the dependency graph before you run the application. You usually do this in the App.xaml.cs context. You would then let the method that composes the graph (controls the lifetime of the IoC container) return the instance of MainView and then show MainView by calling Window.Show() explicitly. This means (as I said before) for static view like MainView you can use dependency injection to pass in the view model. Usually you do this for the application root (MainView/MainViewModel). You can then set the DataContext from code-behind. – BionicCode Oct 21 '21 at 10:45
  • But for dynamic views, let WPF set the DataContext by using DataContext inheritance and DataTemplate. – BionicCode Oct 21 '21 at 10:45
  • Usually you would use composition to build the view model class structure. This means you can inject all view model classes into the MainViewModel, which then exposes each for data binding via a public read-only property. Now by having the IoC container compose the MainView and MainViewModel you compose the application (basically). Otherwise you need abstract factories to control/defer the moment of instantiation. – BionicCode Oct 21 '21 at 10:50
  • At least don't use the Locator. – BionicCode Oct 21 '21 at 10:51
  • The NullReferenceException is easy to track down. I mean the appliocation halts exactly where this exception is thrown. Then ensure the referenced value is properly initialized or add a null check. – BionicCode Oct 21 '21 at 10:54
  • @BionicCode Thank you for all your help! – Melissa Oct 21 '21 at 12:08

0 Answers0