Pass parameter to a constructor in the ViewModel

2020-04-19 05:00发布

I am building a WPF browser application with MVVM pattern.

I have a first page (ConsultInvoice) with a dataGrid. When I double click on one of the row I want to navigate to another page (EditInvoice) passing the selected row in argument to my constructor.

I know if I want do things properly I should use a dependency injection, but I don't really see how to use it here.

How can I simply pass this constructor?

ConsultInvoiceViewModel

private Invoice _selected;
public Invoice Selected
{
    get
    {
        return _selected;
    }
    set
    {
        _selected = value;
        OnPropertyChanged("Selected");
    }
}

private void Edit()
{
    EditInvoiceViewModel editInvoice = new EditInvoiceViewModel(Selected); 
   /* doing something here*/
}

public ICommand EditCommand
{
    get
    {
        return editCommand ?? (editCommand = new RelayCommand(p => this.Edit(), p => this.CanEdit()));
    }
}

EditInvoiceViewModel

public class EditInvoiceViewModel : ViewModelBase
{
    public Context ctx = new Context();
    Invoice invoice;
    PreInvoice preInvoice;
    #region properties
    private ObservableCollection<PreInvoice> collection;
    public ObservableCollection<PreInvoice> Collection
    {
        get
        {
            return collection;
        }
        set
        {
            collection = value;
            OnPropertyChanged("Collection");
        }
    }
    #endregion
    public EditInvoiceViewModel(Invoice inv)
    {
        /* do stuff*/
    }
}

1条回答
混吃等死
2楼-- · 2020-04-19 05:54

Basically you should avoid passing such parameters into the ViewModels constructor, as wiring it with Inversion of Control/Dependency Injection becomes a pain. While you can use Abstract Factory pattern to resolve objects with runtime parameters, it's imho not suitable for ViewModels.

Instead I always suggest using a form of navigation pattern, similar to how Microsoft's Patterns & Practices team has done with Prism. There you have an INavigationAware interface which your ViewModels can implement. It has 2 methods, NavigateTo and NavigateFrom.

And there is a navigation service. The navigation service will switch the views and before switching calling NavigateFrom in the current ViewModel (if it implements it. One can use it to check if data is saved and if necessary cancel the navigation. After the new View has been loaded and the ViewModel assigned to it, call NavigateTo in the newly navigated ViewModel.

Here you'd pass the parameters required for the ViewModel, in your case invoiceId. Try avoid passing whole models or complex objects. Use the invoiceid to fetch the invoice data and to populate your editing ViewModel.

A basinc implementation from my former answer (can be found here):

public interface INavigationService 
{
    // T is whatever your base ViewModel class is called
    void NavigateTo<T>() where T ViewModel;
    void NavigateToNewWindow<T>();
    void NavigateToNewWindow<T>(object parameter);
    void NavigateTo<T>(object parameter);
}

public class NavigationService : INavigationService
{
    private IUnityContainer container;
    public NavigationService(IUnityContainer container) 
    {
        this.container = container;
    }
    public void NavigateToWindow<T>(object parameter) where T : IView
    {
        // configure your IoC container to resolve a View for a given ViewModel
        // i.e. container.Register<IPlotView, PlotWindow>(); in your
        // composition root
        IView view = container.Resolve<T>();

        Window window = view as Window;
        if(window!=null)
            window.Show();

        INavigationAware nav = view as INavigationAware;
        if(nav!= null)
            nav.NavigatedTo(parameter);
    }
}

// IPlotView is an empty interface, only used to be able to resolve
// the PlotWindow w/o needing to reference to it's concrete implementation as
// calling navigationService.NavigateToWindow<PlotWindow>(userId); would violate 
// MVVM pattern, where navigationService.NavigateToWindow<IPlotWindow>(userId); doesn't. There are also other ways involving strings or naming
// convention, but this is out of scope for this answer. IView would 
// just implement "object DataContext { get; set; }" property, which is already
// implemented Control objects
public class PlotWindow : Window, IView, IPlotView
{
}

public class PlotViewModel : ViewModel, INotifyPropertyChanged, INavigationAware
{
    private int plotId;
    public void NavigatedTo(object parameter) where T : IView
    {
        if(!parameter is int)
            return; // Wrong parameter type passed

        this.plotId = (int)parameter;
        Task.Start( () => {
            // load the data
            PlotData = LoadPlot(plotId);
        });
    }

    private Plot plotData;
    public Plot PlotData {
        get { return plotData; }
        set 
        {
            if(plotData != value) 
            {
                plotData = value;
                OnPropertyChanged("PlotData");
            }
        }
    }
}

An example of the INavigationAware interface used in Prism can be found on the projects github repository.

This makes it easy to pass parameter and async load your data (where there isn't any clean way to do this via constructor, as you can't await an async operation inside the constructor without locking, and doing this kind of things in the constructor is very discouraged).

查看更多
登录 后发表回答