为什么 C# WPF 按钮绑定命令在使用简单注入器后不会更改视图?

Why C# WPF button binding command won't change view after using simple injector?

我使用以下文章作为入门代码:
Navigating Between Views in WPF MVVM
Simple Injector WPF Integration

Objective:
尝试使用按钮绑定命令和简单注入器将依赖项注入视图,以 WPF 形式从视图 1 转到视图 2。注意:这些依赖项是存储来自外部来源的数据的存储库。

问题:
在使用 Simple Injector 将依赖项注入到我的 MainWindow 和 MainWindowViewModel 之后,我的按钮不再更改我的当前视图(到我的另一个视图)。当使用 Visual Studio 并使用断点进行调试时,代码似乎永远停留在 RelayCommand.cs 的 CanExecute 函数中(参见 Navigating Between Views in WPF MVVM),其中有东西在调用它一遍又一遍地。我无法对 CanExecute 函数进行更多调试,因为有很多代码被传递(来自 DLL 等)。当不使用断点时,它就好像我的按钮什么也没做一样。

我在输出中没有按钮错误 window 并且没有抛出异常。命令绑定正在运行,因为我可以看到在调试时调用了 MainWindowViewModel.cs 中的函数 OnGo2Screen。调用 OnGo2Screen 后,它按预期移动代码,直到卡在 CanExecute.

我试过的
我检查了主窗口的数据上下文,发现它具有所有正确的功能。

我为 Navigating Between Views in WPF MVVM 文章创建了一个单独的项目,我能够很好地改变视图。但是每当我尝试使用 Simple Injector 时,我的按钮就会坏掉。

我注意到当不使用 Simple Injector 时,代码从 CanExecute 函数移动到 CanExecuteChanged EventHandler 并删除和添加修改器,然后按预期更改视图。但是当使用 Simple Injector 时,它不会这样做。

代码
我正在使用我的 App.xaml.cs 作为启动程序,其中我的 App.xaml 的构建操作为 "Page"。

SimulationCaseView 是视图 1(默认起始视图)。
StreamsView 是视图 2(只是另一个视图)。
UserControl3 是视图 3(只是另一个视图)。

下面是我的代码。请参阅为任何剩余代码提供的两个链接,因为我的许多功能都基于此。

App.xaml

<Application x:Class="MyApp.Desktop.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:views="clr-namespace:MyApp.Desktop.Views">
    <Application.Resources>
        <DataTemplate DataType="{x:Type views:SimulationCaseViewModel}">
            <views:SimulationCaseView />
        </DataTemplate>
        <DataTemplate DataType="{x:Type views:StreamsViewModel}">
            <views:StreamsView />
        </DataTemplate>
        <DataTemplate DataType="{x:Type views:UserControl3ViewModel}">
            <views:UserControl3 />
        </DataTemplate>
    </Application.Resources>
</Application>

App.xaml.cs

namespace MyApp.Desktop
{
    /// <summary>
    /// Interaction logic for App.xaml
    /// </summary>
    public partial class App : Application
    {
        App()
        {
            InitializeComponent();
        }

        [STAThread]
        static void Main()
        {
            var container = Bootstrap();

            // Any additional other configuration, e.g. of your desired MVVM toolkit.

            RunApplication(container);
        }


        private static Container Bootstrap()
        {
            // Create the container as usual.
            var container = new Container();

            // Register your types, for instance:
            container.Register<IPreferencesRepository, PreferencesRepository>(Lifestyle.Singleton);
            container.Register<IStreamRepository, StreamRepository>(Lifestyle.Singleton);

            // Register your windows and view models:
            container.Register<MainWindow>();
            container.Register<MainWindowViewModel>();

            container.Verify();

            return container;
        }

        private static void RunApplication(Container container)
        {
            try
            {
                var app = new App();
                var mainWindow = container.GetInstance<MainWindow>();
                MainWindowViewModel viewModel = container.GetInstance<MainWindowViewModel>();
                mainWindow.DataContext = viewModel;
                app.Run(mainWindow);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
            }
        }
    }
}

MainWindow.xaml

<Window x:Class="MyApp.Desktop.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:MyApp.Desktop"
        mc:Ignorable="d"
        Title="MainWindow"
        Height="350" Width="525"
        xmlns:views="clr-namespace:MyApp.Desktop.Views">
    <Grid>
        <ContentControl Content="{Binding CurrentPageViewModel}" />
    </Grid>
</Window>

MainWindowViewModel.cs

namespace MyApp.Desktop.Views
{
    public class MainWindowViewModel : BaseViewModel
    {
        private IPageViewModel _currentPageViewModel;
        private List<IPageViewModel> _pageViewModels;

        public List<IPageViewModel> PageViewModels
        {
            get
            {
                if (_pageViewModels == null)
                    _pageViewModels = new List<IPageViewModel>();

                return _pageViewModels;
            }
        }

        public IPageViewModel CurrentPageViewModel
        {
            get
            {
                return _currentPageViewModel;
            }
            set
            {
                _currentPageViewModel = value;
                OnPropertyChanged("CurrentPageViewModel");
            }
        }

        private void ChangeViewModel(IPageViewModel viewModel)
        {
            if (!PageViewModels.Contains(viewModel))
                PageViewModels.Add(viewModel);

            CurrentPageViewModel = PageViewModels
                .FirstOrDefault(vm => vm == viewModel);
        }

        private void OnGo1Screen(object obj)
        {
            ChangeViewModel(PageViewModels[0]);
        }

        private void OnGo2Screen(object obj)
        {
            ChangeViewModel(PageViewModels[1]);
        }
        private void OnGo3Screen(object obj)
        {
            ChangeViewModel(PageViewModels[2]);
        }

        public MainWindowViewModel(IStreamRepository streamRepository)
        {
            // Add available pages and set page
            PageViewModels.Add(new SimulationCaseViewModel(streamRepository));
            PageViewModels.Add(new StreamsViewModel());
            PageViewModels.Add(new UserControl3ViewModel());

            CurrentPageViewModel = PageViewModels[0];

            Mediator.Subscribe("GoTo1Screen", OnGo1Screen);
            Mediator.Subscribe("GoTo2Screen", OnGo2Screen);
            Mediator.Subscribe("GoTo3Screen", OnGo3Screen);
        }
    }
}

SimulationCaseView.xaml

<UserControl x:Class="MyApp.Desktop.Views.SimulationCaseView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:local="clr-namespace:MyApp.Desktop"
             mc:Ignorable="d"
             d:DesignHeight="280" d:DesignWidth="280">
    <Grid>
        <Button
            Content="Go to Streams"
            Command="{Binding GoTo2}"
            Width="90" Height="30" Margin="166,220,24,30">
        </Button>
    </Grid>
</UserControl>

SimulationCaseViewModel.cs

namespace MyApp.Desktop.Views
{
    public class SimulationCaseViewModel : BaseViewModel, IPageViewModel
    {
        private ICommand _goTo2;
        private readonly IStreamRepository _repo;
        public SimulationCaseViewModel(IStreamRepository repo)
        {
            _repo = repo;
            Application application = _repo.GetApplicationReference();
            CurrentSimulationCases = new ObservableCollection<SimulationCase>();
            Streams = new ObservableCollection<object>();
            foreach (SimulationCase simulationCase in application.SimulationCases)
            {
                CurrentSimulationCases.Add(simulationCase);
            }
            //FetchStreams = new RelayCommand(OnFetch);
        }

        public ObservableCollection<SimulationCase> CurrentSimulationCases { get; set; }
        public ObservableCollection<object> Streams { get; private set; }

        public ICommand GoTo2
        {
            get
            {
                return _goTo2 ?? (_goTo2 = new RelayCommand(x =>
                {
                    Mediator.Notify("GoTo2Screen", "");
                }));
            }
        }
    }
}

对于按钮为何不起作用的任何帮助,我们将不胜感激。谢谢。

如何解决?

更新命令状态后调用此方法:

CommandManager.InvalidateRequerySuggested();

为什么不更新?

命令仅在这些一般事件发生时更新:

  • KeyUp
  • MouseUp
  • GotKeyboardFocus
  • LostKeyboardFocus

查看源代码了解详情:CommandDevice.cs

对于其他控件,需要刷新的事件较多:

  • 增加长按时重复的RepeatButton
  • DataGrid ...
  • SinglePageViewer ...

您可以双击this linkCommandManager.InvalidateRequerySuggested()方法查看其他刷新命令状态的事件。

因此,如果您的更新不在这些事件中发生,您的命令状态将不会更新。

其他信息

你说在Visual Studio的Visual Studio函数中使用断点调试时,代码好像永远卡在了RelayCommand.cs的CanExecute函数中。

这不是 CanExecute 的循环,它是 GotKeyboardFocusLostKeyboardFocus 事件,当您的活动 window 在您的应用程序和 Visual Studio 之间切换时.

简答

问题出在您的 ViewModel 的 Lifestyle 中,它必须设置为 Singleton 而不是默认的 Transient

    private static Container Bootstrap()
    {
        // Create the container as usual.
        var container = new Container();

        // Register your types, for instance:


        // Register your windows and view models:
        //container.Register<MainWindow>(Lifestyle.Singleton); //not needed
        container.Register<MainWindowViewModel>(Lifestyle.Singleton);

        container.Verify();

        return container;
    }

然后就可以通过简单的方式启动应用了

    private static void RunApplication(Container container)
    {
        try
        {
            var mainWindow = container.GetInstance<MainWindow>();
            var app = new App();
            app.InitializeComponent();
            app.Run(mainWindow);
        }
        catch (Exception ex)
        {
            //Log the exception and exit
            Debug.WriteLine(ex.Message);
        }
    }

完整代码在 github

长答案 - 长话短说

当您在 Bootstrap 中调用 container.Verify 时,您将创建一个 MainWindowViewModel 的实例来验证其实例化,并创建另一个实例来验证 MainWindow class.

顺便说一句,您可以通过不验证容器来解决您的问题!

所以2第二个解是

        //container.Register<MainWindow>(); // => Lifestyle.Transient;
        container.Register<MainWindowViewModel>(); // => Lifestyle.Transient;

        //container.Verify();

现在,请注意您在 MainWindowViewModel c.tor 中有 Mediator 订阅。

    public static void Subscribe(string token, Action<object> callback)
    {
        if (!pl_dict.ContainsKey(token))
        {
            var list = new List<Action<object>>();
            list.Add(callback);
            pl_dict.Add(token, list);
        }
        else
        {
            bool found = false;
            //foreach (var item in pl_dict[token])
            //    if (item.Method.ToString() == callback.Method.ToString())
            //        found = true;
            if (!found)
                pl_dict[token].Add(callback);
        }
    }

foreach 循环 - 我只在上面评论过(它是解决您的问题的 3rd 替代选项) - 会让你跳过对第二个正确的 ViewModel 方法的调用,并会留下第一个错误的方法(记住 Bootstrap 验证创建了它两次)。 如果你想要第 4 个替代解决方案,使用 Mediator pattern

的 classical IComponent 接口
public interface IComponent
{
     void OnGo1Screen(object obj);
     void OnGo2Screen(object obj);
}
public class MainWindowViewModel : BaseViewModel, IComponent

您也可以将订阅移出 c.tor

  public MainWindowViewModel()
  {
     // Add available pages and set page
     PageViewModels.Add(new UserControl1ViewModel());
     PageViewModels.Add(new UserControl2ViewModel());

     CurrentPageViewModel = PageViewModels[0];

     //Mediator.Subscribe("GoTo1Screen", OnGo1Screen);
     //Mediator.Subscribe("GoTo2Screen", OnGo2Screen);
  }

进入你的 Program:

            var context = mainWindow.DataContext as IComponent;
            Mediator.Subscribe("GoTo1Screen", context.OnGo1Screen);
            Mediator.Subscribe("GoTo2Screen", context.OnGo2Screen);