如何使用 MVVM 在 WPF 中正确实现导航侧边栏菜单(+找出更改应用程序中视图的正确方法)?

问题描述 投票:0回答:1

如果有人可能觉得标题令人困惑,我想道歉,我在这里描述的问题比我想象的要复杂,我很难在标题中总结它。

基本上我有一个带有主屏幕的应用程序,它由一个主空屏幕组成,用户可以在其上显示其他视图和侧面的侧导航面板。但是,当我打开主屏幕部分上显示的不同视图时,它也恰好覆盖了导航面板的扩展部分。发生这种情况的原因非常明显。

为了解决这个问题,我决定将导航面板和主屏幕分成两个单独的视图,以便更容易地操作视图的层次结构,以便导航面板始终位于顶部,位于所有其他显示的视图之上。令我失望的是,这样的选项似乎不存在,所以我想出了另一个想法 - 在按下“打开菜单”按钮时切换活动视图,并在再次按下时将其更改回当前视图。

这就是带有侧面导航面板的视图现在的样子:

<UserControl x:Class="ApkaJezykowa.MVVM.View.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:ApkaJezykowa"
        xmlns:viewModel="clr-namespace:ApkaJezykowa.MVVM.ViewModel"
        xmlns:view="clr-namespace:ApkaJezykowa.MVVM.View"
        mc:Ignorable="d"
             HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
        Background="Transparent">
    <UserControl.DataContext>
        <viewModel:MainViewModel/>
    </UserControl.DataContext>
    <Border Background="Transparent">
        <Grid>
            <Grid>

                <Grid x:Name="nav_pnl" Width="80" Background=" #484848" HorizontalAlignment="Left" Grid.ZIndex="4">
                    <StackPanel x:Name="st_pln">
                        <Grid Height="80">
                            <TextBlock
                                    Grid.Row="1"
                                    Grid.Column="0"
                                    Margin="85,0,0,0"
                                    Text="Menu"
                                    FontSize="22"
                                    HorizontalAlignment="Left"
                                    VerticalAlignment="Center"
                                    Style="{StaticResource Font_Style}">
                                <TextBlock.Effect>
                                    <DropShadowEffect BlurRadius="10"
                                                          ShadowDepth="1"
                                                          Direction="-90"
                                                          Color="White"/>
                                </TextBlock.Effect>
                            </TextBlock>
                            <ToggleButton x:Name="tg_btn"
                                              Height="60"
                                              Width="60"
                                              BorderThickness="0"
                                              HorizontalAlignment="Left"
                                              Margin="10,0,0,0"
                                              Style="{StaticResource tb_style}" Unchecked="tg_btn_Unchecked" Checked="tg_btn_Checked">
                                <ToggleButton.Background>
                                    <ImageBrush ImageSource="pack://application:,,,/ApkaJezykowa;component/Images/menu.png" Stretch="Uniform"/>
                                </ToggleButton.Background>
                                <ToggleButton.Triggers>
                                    <EventTrigger RoutedEvent="ToggleButton.Unchecked">
                                        
                                        <BeginStoryboard>
                                            <Storyboard x:Name="HideStackPanel">
                                                <DoubleAnimation
                                                        Storyboard.TargetName="nav_pnl"
                                                        Storyboard.TargetProperty="Width"
                                                        BeginTime="0:0:0"
                                                        From="260" To="80"
                                                        Duration="0:0:0.1"></DoubleAnimation>
                                            </Storyboard>
                                        </BeginStoryboard>
                                    </EventTrigger>
                                    <EventTrigger RoutedEvent="ToggleButton.Checked">
                                        <BeginStoryboard>
                                            <Storyboard x:Name="ShowStackPanel">
                                                <DoubleAnimation
                                                        Storyboard.TargetName="nav_pnl"
                                                        Storyboard.TargetProperty="Width"
                                                        BeginTime="0:0:0"
                                                        From="80" To="260"
                                                        Duration="0:0:0.1"
                                                    ></DoubleAnimation>
                                            </Storyboard>
                                        </BeginStoryboard>
                                    </EventTrigger>
                                </ToggleButton.Triggers>
                            </ToggleButton>
                        </Grid>
                        <ListView x:Name="LV" Background="#484848" ScrollViewer.HorizontalScrollBarVisibility="Disabled" BorderThickness="0" Canvas.ZIndex="3">
                            <--!just a list of options-->
                        </ListView>
                    </StackPanel>
                </Grid>
            </Grid>
            <Grid x:Name="img_bg" Margin="80,0,0,0" PreviewMouseLeftButtonDown="BG_PreviewMouseLeftButtonDown">
                <view:MainScreenView Panel.ZIndex="1"/>
                <ContentControl Content="{Binding SelectedViewModel}" Panel.ZIndex="1"/>
            </Grid>
            <ContentControl Content="{Binding MainView}"/>
        </Grid>
    </Border>

</UserControl>

但是后来我遇到了当前设计的缺陷。每个视图的 ViewModel 都有自己的 UpdateViewCommand,它有自己的触发器,决定接下来应该显示哪个视图。

导航面板的示例:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;
using ApkaJezykowa.MVVM.ViewModel;

namespace ApkaJezykowa.Commands
{
    internal class UpdateViewCommand : ICommand
    {
        private MainViewModel viewModel;

        public UpdateViewCommand(MainViewModel viewModel)
        {
            this.viewModel = viewModel;
        }

        public event EventHandler CanExecuteChanged;

        public bool CanExecute(object parameter)
        {
            return true;
        }
        public void Execute(object parameter)
        {
            if(parameter.ToString() == "Lessons")
            {
                viewModel.SelectedViewModel = new LessonsViewModel();
            }
            if (parameter.ToString() == "Dictionary")
            {
                viewModel.SelectedViewModel = new DictionaryViewModel();
            }
            if (parameter.ToString() == "Info")
            {
              viewModel.SelectedViewModel = new InfoViewModel();
            }
            if (parameter.ToString() == "Settings")
            {
              viewModel.SelectedViewModel = new SettingsViewModel();
            }
            if(parameter.ToString() == "Editor")
            {
              viewModel.SelectedViewModel = new LessonEditorViewModel();
            }
        }
    }
}

...我有大约两打。更改以这种方式设计的视图本质上阻止了我设置此解决方案,因为导航面板本质上需要知道哪个 ViewModel 当前处于活动状态。

所以,目前我看到解决这个混乱局面的两到三种方法:

  1. 要么弄清楚如何操作 ViewModel 的层次结构,并使导航始终通过某个参数进行选择(我完全不知道如何实现),
  2. 或者将更新视图命令系统完全重写为更灵活的东西,这也将允许我使用动态选择的视图模型来实现这个想法,并总体上使整个应用程序代码不那么混乱(我也不知道如何实现),
  3. 对整个侧面导航面板进行彻底重新设计,因为我认为当前的设计也可能存在缺陷。

我希望我的问题的解释足够清楚以便于理解。一如既往,提前感谢您帮助我解决这个问题的任何尝试,无论成功与否。

c# wpf xaml mvvm
1个回答
0
投票

我强烈怀疑您将 ViewModel 与从代码隐藏中提取的 UI 代码混淆了。 WPF(适用于所有 XAML 平台)中 ViewModel 的功能是在其属性中反映 Model。 ViewModel 不应该跟踪视图、以某种方式切换它们、以任何其他方式管理它们。

理论上,您应该让某些 Model 根据 ViewModel 请求更改其状态。这些模型状态应该反映在 ViewModel 属性(或多个属性)中。跟踪这些属性的视图将自动更新,包括替换窗口区域(或多个区域)中的视图。

具体到你的实现,如果你不考虑我上面描述的概念错误,那么你需要使用标准实现命令。例如,您可以从这里获取:我的基类实现示例:BaseInpc、RelayCommand、RelayCommandAsync、RelayCommand、RelayCommandAsync

并在View级别实现一个单独的“Navigator”类,该类将在App中初始化。

using Simplified;
using System.Windows.Markup;

namespace Core2024.SO
{
    [ContentProperty(nameof(ViewModelTypes))]
    public class SimpleNavigator : BaseInpc
    {
        public Dictionary<string, Type> ViewModelTypes { get; } = [];
        private object? _currentViewModel;
        private RelayCommand<string>? _navigateCommand;

        public object? CurrentViewModel { get => _currentViewModel; private set => Set(ref _currentViewModel, value); }

        public RelayCommand NavigateCommand => _navigateCommand
            ??= new RelayCommand<string>
            (
                name =>
                {
                    Type viewModelType = ViewModelTypes[name];

                    var ctor = viewModelType.GetConstructor([]);
                    CurrentViewModel = ctor!.Invoke([]);
                },
                ViewModelTypes.ContainsKey
            );
    }
}
<Application --------------------
             --------------------
             Startup="OnStartup">
    <Application.Resources>
        <so:SimpleNavigator x:Key="navigator"/>
        private void OnStartup(object sender, StartupEventArgs e)
        {
            SimpleNavigator navigator =(SimpleNavigator) FindResource(nameof(navigator));
            navigator.ViewModelTypes["Lessons"] = typeof(LessonsViewModel);
            navigator.ViewModelTypes["Dictionary"] = typeof(DictionaryViewModel);
            navigator.ViewModelTypes["Info"] = typeof(InfoViewModel);
        }

用途:

<ContentControl Content="{Binding CurrentViewModel,
                                  Source{StaticResource navigator}}"
                Panel.ZIndex="1"/>

在任何级别视图中:


    <Button Command="{Binding NavigateCommand,
                              Source{StaticResource navigator}}"
            CommandParameter="ViewModel key"/>
© www.soinside.com 2019 - 2024. All rights reserved.