First of all, your view should not depend on any application setting models. For this reason, the dependency of your Window
and Page
instances on an IConfiguration
object in an MVVM application is wrong. Configuration is part of the application Model.
To avoid such dependencies the best way is to use the so called View-Model-First pattern. The pattern requires that your View Model classes decide which View class to load. This is the opposite behavior of your current View-First pattern where the View decides which View Model to load.
View-Model-First is usually implemented by binding a View Model class to the View (for example a ContentPresenter
or ContentControl
or a derived class). Then define a DataTemplate
that will be loaded and rendered by the framework based on the current data type (view model type). This DataTemplate
contains the actual view you want to show.
This eliminates the dependency of the View on Model classes (which in this case is introduced by allowing the view to construct its View Model classes explicitly.
You can check this example to get an idea on View-Model-First: C# WPF Navigation Between Pages (Views).
For this reasons I recommend to change your approach and implement the MVVM friendly View-Model-First pattern.
Even if you don't implement the MVVM pattern you will achieve an application that is easier to maintain and to extend.
Whether you use View-First or View-Model-First, the IConfiguration
instance must come from the application Model.
First fix or improvement of your current code is to move the IConfiguration
instantiation to the Model. The View Model must get such application data from the Model and not the View. For this purpose the example creates an ConfigurationRepositoty
class that encapsulates creation of IConfiguration
instances. Because data persistence is also part of the Model the ConfigurationRepositoty
has easy access to the resources like files or database. Ideally, it would use other Model classes that know in detail how to get the data (read from file or database).
ConfigurationRepositoty
is either created directly in the View Model class or injected via the constructor (in case of implementing any IoC like the Dependency Injection pattern).
Regarding view model constructor dependencies, you should introduce a factory where the view model has constructor dependencies. Consider to implement the Abstract Factory pattern. A result of your View-First implementation is that the View instantiates the views explicitly and therefore would have to know all the dependencies in order to satisfy the constructors.
Because the dependencies of View Model classes are usually Model classes, those dependencies when declared in the View are illegal or compromising in terms of design. When using factories (or abstract factories), those dependencies are eliminated.
The second mandatory fix is to move the page navigation from View Model to the View.
Because you implement a View-First navigation, you navigate by creating instances of views or by explicitly knowing them by their URI.
But your View Model must not know/depend on View types or their URIs. This disqualifies them to act as a navigation controller.
If you want to navigate from the View Model then implement the View-Model-First pattern.
Lastly, the MyFrameContent
property looks highly suspicious too. Based on the name it appears that the owner view model class has too much knowledge about the view, maybe even concrete dependencies.
ConmfigurationRepository.cs
A Model class that knows how to obtain or create the IConfiguration
instance.
public class ConmfigurationRepository
{
private static Lazy<IConfiguration> SharedConfiguration { get; }
= new Lazy<IConfiguration>(InitializeSharedConfiguration);
public IConfiguration GetApplicationConfiguration()
=> this.SharedConfiguration;
// In case there are different implementations of IConfiguration
// for example when there are different configuration contexts
// then expose a related API
public IConfiguration GetHomePageConfiguration()
{
// TODO::Create or get a new/shared IConfiguration instance.
// If the configuration comes from an external source like a file or database,
// then this is the place to read it (using related classes or services)
}
private static IConfiguration InitializeSharedConfiguration()
{
// TODO::Create or get a new/shared IConfiguration instance.
// If the configuration comes from an external source like a file or database,
// then this is the place to read it (using related classes or services)
}
}
MainWindow.xaml.cs
partial class MainWindow : Window
{
private void ShowView(string viewName)
{
Page destinationPage = viewName switch
{
nameof(HomePage) => CreateHomeView(),
nameof(CheckoutPage) => CreateCheckoutView(),
_ => throw new ArgumentException("View not found", nameof(viewName)),
};
// Show view
this.frame.Content = destinationPage;
}
private Page CreateHomeView()
{
// If HomePageVM has constructor dependencies
// use a factory to instantiate the type
HomePageVM homePageVm = new HomePageVM();
var homeView = new HomeView(homePageVm);
homeView.NavigationRequested += OnNavigationRequested;
return homeView;
}
private Page CreateCheckoutView()
{
// If CheckoutPageVM has constructor dependencies
// use a factory to instantiate the type
CheckoutPageVM checkoutPageVm = new CheckoutPageVM();
return new CheckoutPage(checkoutPageVm);
}
private void OnNavigationRequested(object sender, NavigationRequestedEventArgs e)
{
var navigationSource = (INavigationSource)sender;
navigationSource.NavigationRequested -= OnNavigationRequested;
ShowView(e.DestinationViewName);
}
}
HomePage.cs
public partial class HomePage : Page, INavigationSource
{
public HomeWindow(HomePageVM homePageVM)
{
InitializeComponent();
homePageVM.CheckedOut += OnCheckedOut;
this.DataContext = homePageVM;
}
private void OnCheckedOut(object sender, EventArgs e)
{
((HomePageVM)this.DataContext).CheckedOut -= OnCheckedOut;
OnNavigationRequested(nameof(CheckoutPage));
}
private void OnNavigationRequested(string destinationViewName)
=> this.NavigationRequested?.Invoke(this, new NavigationRequestedEventArgs(destinationViewName));
}
CheckoutPage.cs
public partial class CheckoutPage : Page, INavigationSource
{
public CheckoutPage(CheckoutPageVM checkoutPageVM)
{
InitializeComponent();
this.DataContext = checkoutPageVM;
}
}
INavigationSource.cs
To be implemented by all pages that need to navigate away.
public interface INavigationSource
{
public event EventHandler<NavigationRequestedEventArgs> NavigationRequested;
}
NavigationRequestedEventArgs.cs
public class NavigationRequestedEventArgs : EventArgs
{
public DestinationViewName DestinationViewName { get; }
public NavigationRequestedEventArgs(string destinationViewName)
{
this.DestinationViewName = destinationViewName;
}
}
HomePageVM.cs
public class HomePageVM
{
readonly IConfiguration _config;
public HomePageVM()
{
_config = new ConmfigurationRepository().GetApplicationConfiguration();
CheckoutClickCommand = new MyCommand(ExecuteCheckoutCommand);
ShipmentClickCommand = new MyCommand(ShipmentClick);
}
private void ExecuteCheckoutCommand()
=> OnCheckedOut();
private void OnCheckedOut()
=> this.CheckedOut?.Invoke(this, EventArgs.Empty);
}
CheckoutPageVM.cs
public class CheckoutPageVM
{
readonly IConfiguration _config;
public CheckoutPageVM()
{
_config = new ConmfigurationRepository().GetApplicationConfiguration();
}
}