1

I can easily pass in an IConfiguration appsettings.json to a Window in WPF, which is what I have done in the past. Now I am trying to do this the proper way and pass it into a ViewModel that is assigned as the DataContext of a Page that is being displayed on a Window. I'm loading the Page into a Frame on the Window. I would like to pass the config to the ViewModel of the Page so I dont have to reread it in manually.

Here is what I currently have and some stuff I tried:

    public partial class HomeWindow : Window
    {
        readonly IConfiguration _config;
        
        public HomeWindow(IConfiguration config)
        {
            _config = config;

            // Instantiate the Object with JSON settings.
            InitializeComponent();
            DataContext = new HomeWindowVM(config)
        } 
    }


    public class HomeWindowVM
    {
        readonly IConfiguration _config;
        //public INotify btnCheckoutPage { get; set; }
        public object MyFrameContent { get; set; }

        public HomeWindowVM(IConfiguration config)
        {
            _config = config;
            CheckoutClickCommand = new MyCommand(CheckoutClick);
            ShipmentClickCommand = new MyCommand(ShipmentClick);
            frameURI = new INotify();
            frameVis = new INotify();
            frameVis.MyProperty = "Hidden";
        }

        private void CheckoutClick()
        {
            frameURI.MyProperty = "CheckoutPage.xaml";
            frameVis.MyProperty = "Visible";
// I tried this 
            //CheckoutPage checkoutPage = new CheckoutPage(_config);
            //MyFrameContent = new CheckoutPage(_config);

        }
    }


    public partial class CheckoutPage : Page
    {
        readonly IConfiguration _config;

        public CheckoutPage()
        {
            InitializeComponent();
            DataContext = new CheckoutPageVM();
        }
        public CheckoutPage(IConfiguration config)
        {
            _config = config;
            InitializeComponent();

            //Frame frame = new Frame();
            //MainView 
            DataContext = new CheckoutPageVM(config);
        }
    }


    public class CheckoutPageVM 
    {
        readonly IConfiguration _config;

        public CheckoutPageVM(IConfiguration config)
        {
            _config = config;
            LblErrorProjectNum = new INotify();
            LblErrorProjectNum.MyProperty = "Hidden";
            LblErrorTransfer = new INotify();
            LblErrorTransfer.MyProperty = "Hidden";
            CheckoutSubmitCommand = new MyCommand(CheckoutSubmit);            
        }
Bigbear
  • 489
  • 2
  • 5
  • 21

2 Answers2

1

There's no one proper way to do this, but I'm guessing you've got a background in ASP.NET and are familiar with dependency injection. Of course WPF doesn't use that pattern and it's probably overkill in this case. A statically accessible singleton works perfectly fine and avoids the morass of passing the config instance around through constructors or other over-engineered solutions:

// In your Model layer
public static class MyConfiguration
{
     public static IConfiguration Instance { get; private set; }

     public static void Load(IConfiguration config) 
     {
          // call this when the app initializes
          Instance = config;
     }
}

Then if individual classes need instance properties for binding, etc., it's as simple as:

public partial class HomeWindow : Window
{
    public IConfiguration Config => MyConfiguration.Instance;
    // ...
}

public class CheckoutPageVM 
{
    public IConfiguration Config => MyConfiguration.Instance;
    // ...
}

public partial class CheckoutPage : Page
{
    public IConfiguration Config => MyConfiguration.Instance;
    // ...
}

etc.

Emperor Eto
  • 2,456
  • 2
  • 18
  • 32
0

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();
  }
}
BionicCode
  • 1
  • 4
  • 28
  • 44