【问题标题】:MVVM Navigation Parent and Child ViewsMVVM 导航父视图和子视图
【发布时间】:2016-04-10 21:42:13
【问题描述】:

我有一个作为父视图的 GeneralView,打开时会打开父视图,然后是子视图。我想实现导航并将按钮保留在侧面(如 UserPage)。让我们转到所需的行为,然后是我现在拥有的代码。

我实现 ChildView 的方式没有改变,保持在 HomeView aka FriendsView 中。

所以描述登录> GeneralView(即在Home 中立即打开)> 点击About 并且childView 变为AboutView,点击home 再次显示HomeView。

我有什么:

常规视图

<UserControl x:Class="WpfWHERE.View.GeneralView"
         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:WpfWHERE.View"
         xmlns:ViewModel="clr-namespace:WpfWHERE.ViewModel"
         mc:Ignorable="d" 
         d:DesignHeight="600" d:DesignWidth="800">
<UserControl.DataContext>
    <ViewModel:GeneralViewModel/>
</UserControl.DataContext>
<UserControl.Resources>
    <DataTemplate DataType="{x:Type ViewModel:FriendsViewModel}">
        <local:FriendsView />
    </DataTemplate>
    <DataTemplate DataType="{x:Type ViewModel:AboutViewModel}">
        <local:AboutView />
    </DataTemplate>
</UserControl.Resources>
<DockPanel Margin="0,0,0,0">
    <StackPanel Orientation="Horizontal">
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" MaxWidth="200"/>
            </Grid.ColumnDefinitions>
            <Image Grid.Column="1" x:Name="userImage" Source="/Resources/Images/profileImage.png" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Height="161" Width="180" />
            <Label Grid.Column="1" x:Name="labelName" Content="NameHere" HorizontalAlignment="Left" Margin="10.4,171,0,0" VerticalAlignment="Top" RenderTransformOrigin="0.536,1.344" Height="26" Width="67"/>
            <StackPanel Grid.Column="1" Margin="0.4,202,-1.2,0" VerticalAlignment="Top" Height="200">
                <MenuItem Style="{DynamicResource MyMenuItem}" Command="{Binding DataContext.SelectViewCommand, ElementName=GeneralView}" CommandParameter="FriendsViewModel" Header="Home" x:Name="Home"/>
                <MenuItem Style="{DynamicResource MyMenuItem}" Header="Overview" x:Name="Overview"/>
                <MenuItem Style="{DynamicResource MyMenuItem}" Header="Settings" x:Name="Settings"/>
                <MenuItem Style="{DynamicResource MyMenuItem}" Header="About" Command="{Binding DataContext.SelectViewCommand, ElementName=GeneralView}" CommandParameter="AboutViewModel" x:Name="About"/>
            </StackPanel>
        </Grid>
        <ContentControl Content="{Binding Current_ViewModel}" Height="600" Width="600"/>
    </StackPanel>
</DockPanel>

GeneralViewModel

 class GeneralViewModel:AViewModel
{
    public GeneralViewModel()
    {
        this.AddViewModel(new FriendsViewModel() { DisplayName = "Friends", InternalName = "FriendsViewModel" });
        this.AddViewModel(new AboutViewModel() { DisplayName = "About", InternalName = "AboutViewModel" });
        this.Current_ViewModel = this.GetViewModel("FriendsViewModel");
    }
}

AViewModel 接口

 public abstract class AViewModel : ViewModelBase
{
    public string Name { get; set; }
    public RelayCommand<string> SelectViewCommand { get; set; }

    public AViewModel()
    {
        SelectViewCommand = new RelayCommand<string>(OnSelectViewCommand);
    }

    private static ObservableCollection<ViewModelBase> _ViewModels;
    public static ObservableCollection<ViewModelBase> ViewModels
    {
        get { return _ViewModels; }
        set { _ViewModels = value; }
    }

    public void AddViewModel(ViewModelBase viewmodel)
    {
        if (ViewModels == null)
            ViewModels = new ObservableCollection<ViewModelBase>();

        if (!ViewModels.Contains(viewmodel))
            ViewModels.Add(viewmodel);
    }

    public ViewModelBase GetViewModel(string viewmodel)
    {
        return ViewModels.FirstOrDefault(item => item.InternalName == viewmodel);
    }

    private void OnSelectViewCommand(string obj)
    {
        switch (obj)
        {
            case "ExitCommand":
                Application.Current.Shutdown();
                break;
            default:
                this.Current_ViewModel = this.GetViewModel(obj);
                break;
        }
    }

    private ViewModelBase _Current_ViewModel;
    public ViewModelBase Current_ViewModel
    {
        get { return _Current_ViewModel; }
        set { _Current_ViewModel = value; OnPropertyChanged("Current_ViewModel"); }
    }
}

【问题讨论】:

  • 您面临的具体问题是什么?
  • @sexta13 即使我单击 AboutButton,视图也不会改变停留在 FriendsView 中。请澄清一下,我有两个子视图(朋友和关于)。朋友是在一般加载的同时加载的,如果我点击主页,我也想要它。单击 about 时应该会出现 about。
  • 你必须实现 INotifyProperty...这样视图才能知道发生了什么变化。
  • 我必须在哪里这样做?并从哪里调用事件?
  • 正如@sexta13 提到的,您必须实现 INotifyPropertyChanged 接口。它是 WPF 中双向数据绑定的支柱。不要在不了解这一点的情况下直接使用 MVVM,否则您肯定会遇到障碍。

标签: c# wpf xaml mvvm navigation


【解决方案1】:

试试这个....

改变这个...

        <StackPanel Grid.Column="1" Margin="0.4,202,-1.2,0" VerticalAlignment="Top" Height="200">
            <MenuItem Style="{DynamicResource MyMenuItem}" Command="{Binding DataContext.SelectViewCommand, ElementName=GeneralView}" CommandParameter="FriendsViewModel" Header="Home" x:Name="Home"/>
            <MenuItem Style="{DynamicResource MyMenuItem}" Header="Overview" x:Name="Overview"/>
            <MenuItem Style="{DynamicResource MyMenuItem}" Header="Settings" x:Name="Settings"/>
            <MenuItem Style="{DynamicResource MyMenuItem}" Header="About" Command="{Binding DataContext.SelectViewCommand, ElementName=GeneralView}" CommandParameter="AboutViewModel" x:Name="About"/>
        </StackPanel>

到这里……

        <StackPanel Grid.Column="1" Margin="0.4,202,-1.2,0" VerticalAlignment="Top" Height="200">
            <MenuItem Style="{DynamicResource MyMenuItem}" Command="{Binding SelectViewCommand}" CommandParameter="FriendsViewModel" Header="Home" x:Name="Home"/>
            <MenuItem Style="{DynamicResource MyMenuItem}" Header="Overview" x:Name="Overview"/>
            <MenuItem Style="{DynamicResource MyMenuItem}" Header="Settings" x:Name="Settings"/>
            <MenuItem Style="{DynamicResource MyMenuItem}" Header="About" Command="{Binding SelectViewCommand}" CommandParameter="AboutViewModel" x:Name="About"/>
        </StackPanel>

请注意,我已从您的 MenuItems 中删除了“DataContext”和“ElementName”

INotifyProperty 已在 ViewModelBase 中实现

更新 1

问题在于 ElementName=GeneralView... 具有该名称的元素不存在。您可以将 x:Name=”GeneralView” 添加到 Base_View XAML 的顶部,但没有必要,因为您的 ContentControl 无论如何都绑定到 Base_ViewModel 中的 Current_ViewModel ......

当您按下按钮“更改视图”时,您实际上是在更改您的 ContentControl 绑定到的属性的值,因此您必须在您的 ContentControl 绑定的类的 SAME 实例中调用正确的 SelectViewCommand 函数也……

在演示中,您会在我调用的“LogOn_View”中看到

Command="{Binding DataContext.SelectViewCommand, ElementName=Base_V}", CommandParameter="Main_ViewModel"

这里我调用Base_V中的SelectViewCommand,那是因为我想改变Base_V的ContentControl中显示的视图

在 Main_View 我调用

Command="{Binding SelectViewCommand}", CommandParameter="MainV1_ViewModel"

这里我调用Main_ViewModel中的SelectViewCommand,那是因为我想改变ManiView的ContentControl中显示的View

对于任何想要我上面所说的演示代码的人,你可以在这里找到它......

http://www.mediafire.com/download/3bubiq7s6xw7i73/Navigation1.rar

另外,对代码进行一点更新...用这个替换AviewModel中的AddViewModel函数.....

    public void AddViewModel(ViewModelBase viewmodel)
    {
        if (ViewModels == null)
            ViewModels = new ObservableCollection<ViewModelBase>();

        var currentVNs = (from vms in ViewModels where vms.InternalName == viewmodel.InternalName select vms).FirstOrDefault();
        if (currentVNs == null)
            ViewModels.Add(viewmodel);
    }

【讨论】:

  • 实现了,但他从不调用 RaisePropertyChanged,所以视图不知道要更新什么
  • 蒙蒂,你就是那个人。有效。你能解释一下,以便我理解并且永远不会重复同样的错误吗? @sexta13 你怎么称呼 RaisePropertyChanged ?
  • @monty 很抱歉再次打扰您。但是您知道我可以从另一个视图模型打开视图模型的方法吗?试图更好地解释: OkButton(触发将在 LoginViewModel 中处理的命令)该命令将数据发送到 web 服务,然后它就得到了我想做两件事的答案(从这里打开 viewModel 并发送一个类)这个有可能吗?
  • 没关系,有很多方法可以做到这一点,但是,回到演示代码,“登录”按钮已经调用了“Base_V”中的函数(SelectViewCommand OnSelectViewCommand 函数)。就我个人而言,我会创建功能来验证“AviewModel”中的用户名\密码,并根据用户输入的正确详细信息切换到适当的 ViewModel……好吧,这可能看起来不合逻辑,但它减少了重复代码和所有代码(在视图之间导航)保存在一个地方...
  • 我的主要目标是始终使用现有代码实现解决方案,而不是简单地添加更多代码,这些代码并没有真正增加任何价值,只会使事情变得复杂。
【解决方案2】:

好吧……想了想……网上很多教程代码的问题是太基础了。 Data (Model) View 之间的关系的实现,似乎是一个品味问题。然而,我只见过它做得不好。就我个人而言,我从事过的大多数 WPF 应用程序都是绝对垃圾、无法维护和无法管理的意大利面条代码……您可以清楚地看到,原始开发人员在进行过程中已经“学习”了,无处不在,没有一致性....因为我通常会在一个项目中途进行,尽管我别无选择,只能继续它开始的方式,所以我从来没有考虑过,直到现在!!。

我自己的理解是,“模型”定义了数据结构,“视图”向用户显示数据,而 ViewModel 介于两者之间,将数据(模型)转换为可以显示的内容用户。

在 ViewModel 中,您只需拥有 ObservableCollections(数据列表(模型))和单个数据实例(模型的单个实例)。 “视图”然后绑定到 ObservableCollections(和单个模型实例)以显示该数据(使用 XAML 和模板等的魔力)....

至于将对象(类)传递给 ViewModel,我认为您实际上不需要这样做。您只需在 ViewModel 中创建一个表示您要显示的数据的属性,然后在需要时从“源”检索数据(通常在显示视图时,但也可以是计时器\线程或其他东西的周期性)....

我的 Demo 代码(下载)的主要问题是 ViewModel 的构造函数在应用程序启动后从未被调用,因此无法从数据源刷新 ViewModel 中的属性... .

很可能有更好的方法来做到这一点,但我已经通过在“ViewModelBase”中引入一个名为 Initialize 的事件解决了这个问题

    public delegate void MyEventHadler();
    public event MyEventHadler Initialize;

    public  void InitializeFunction()
    {
        if (Initialize != null)
            Initialize.Invoke();
    }

然后可以在每个 ViewModel 的构造函数中订阅该事件

    public MainV2_ViewModel()
    {
        this.Initialize += MainV2_ViewModel_Initialize; // our new Event
    }

当我们从其他地方导航到这个 ViewModel\View 时调用的事件存根......

    private void MainV2_ViewModel_Initialize()
    {
        // So here we are retrieving a List of All users from the WCF Service
        this.AllUsers = new ObservableCollection<ServiceReference1.User>( DataAccessLevel.sr1.GetAllUsers());
        // Now our AllUsers property has been updated and the View will display the new data
    }

现在,当您从一个 ViewModel\View 切换到另一个时,初始化事件会在 AviewModel\Current_ViewModel 属性设置器中调用\引发

    private ViewModelBase _Current_ViewModel;
    public ViewModelBase Current_ViewModel
    {
        get { return _Current_ViewModel; }
        set {
            _Current_ViewModel = value;
            // the Constructor of the ViewModel never gets called more that once on App Start 
            // so we have to implement/raise our own event when changing from one View to another.
            if (Current_ViewModel != null)
                Current_ViewModel.InitializeFunction(); // InitializeFunction will fire the event in this ViewModel, we can now initialise the properties.
            OnPropertyChanged("Current_ViewModel"); }
    }

这将在被切换的 ViewModel 中触发“初始化”事件,让我们有机会刷新数据.....

演示代码现在是一个功能齐全的应用程序(当然还需要工作)。它提供以下功能...

注册新用户 :: 使用用户名和密码创建新用户

登录用户 :: 使用用户名和密码登录现有用户

恢复用户(忘记密码) :: 重置注册用户的密码(使用现有用户名和新密码)

此外,当出现问题(密码不正确等)时,WCF 服务会返回错误消息,然后显示在错误视图中(请参阅 LogOnError_ViewModel 和 LogOnError_View)

在这里找到演示代码....

http://www.mediafire.com/download/881yo6reo55tm8l/Navigation1_WCF_EF_05052016.rar

演示代码附带 WCF 服务和 WPF 应用程序,因为它使用 WCF 服务,它只能从 Visual Studio IDE 运行(除非您将 WCF 服务部署到 IIS)。 WCF 服务使用实体框架并(应该)在您首次注册新用户时创建\附加数据库来存储用户数据...

也许您可以改进代码或获得一些想法.....

【讨论】:

  • :o 我开始失去所有希望了。要发出网络服务请求,我应该在 LogOn_ViewModel> public LogOnCommandParameter { //Do request here} 中实现?我想传递给另一个视图模型的类不是我在这里拥有的用户(用户名 + 密码),而是由 web 服务回答的那个(我应该替换 OnSelectViewCommand 对吗?
猜你喜欢
  • 2013-11-08
  • 1970-01-01
  • 2023-04-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多