Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
537 views
in Technique[技术] by (71.8m points)

c# - How can I open another view in WPF MVVM using click handlers and commands? (Is my solution reasonable?)

I am writing a WPF application that has two windows.

I have a MainWindowViewModel that holds two more view models: AllTagsViewModel and PlotViewModel.

public AllTagsViewModel AllTagsViewModel { get; private set; }

public PlotViewModel PlotViewModel { get; private set; }

At the moment, I'm using this solution as a click handler in the main window:

private void LaunchPlotWindow_OnClick(object sender, RoutedEventArgs e)
    {
        if (PlotWindow.GlobalInstanceCount == 0)
        {
            PlotWindow plotWindow = new PlotWindow();

            PlotViewModel context = GetViewModel().PlotViewModel;
            plotWindow.DataContext = context; 

            plotWindow.Show();
        }
    }

I am also binding a command to the button. The command is in the MainWindowViewModel and it instantiates a new PlotViewModel using the constructor PlotViewModel(AllTagsViewModel atvm).

The problem with this is that the command setting the data context executes after the click handler. this means that the PlotWindow works as expected the second time it is opened.

What is a better solution for this problem? Can I use an event to keep the AllTagsViewModel in the PlotViewModel up to date at all times with the one in the MainWindowViewModel? My solution at the moment feels like a hack and very poor practice.

Thanks for the advice.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

Foreword: Usually you wouldn't want to have your PlotViewModel and pass it to a window, as it makes a few things more complicated.

There are to basic approaches View-First and ViewModel First. In View-First you create the View (Page, Window etc) and inject the ViewModel into it (usually via constructor). Though this makes it a bit difficult to and pass a parameter object to it.

Which is where the NavigationService comes. You resolve the View via IoC container, then pass a parameter to the ViewModel, i.e. if it's a UserViewModel you'd pass the userId to it and the ViewModel will load the user.

The solution: Navigation Service You can either use an existing one (Prism, or other MVVM Frameworks which come with their own navigation services).

If you want a own simple one, you could create an INavigationService interface and inject it into your ViewModels.

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);
}

and implement it like (I am assuming you use a IoC container, since IoC is a key to MVVM to key the objects decoupled. Example with Unity IoC Container)

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
{
}

and finally you implement your PlotViewModel class and use the passed parameter to load the object

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");
            }
        }
    }
}

Of course could modify the NavigationService to also set the DataContext inside it. Or use strings to resolve the View/Window (such as Prism for Windows Store Apps does).

And in the final code you open the window by calling navigationService.NavigateToWindow<IPlotView>(platId); in your code (i.e. in an ICommand which is bound to a buttons Command Property in your XAML.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...