【问题标题】:calling a method from ViewModel when DataContext changesDataContext 更改时从 ViewModel 调用方法
【发布时间】:2020-04-17 10:57:45
【问题描述】:

情况: 我有一个适用于幻想类的小应用程序。在下面的示例中,我将其归结为裸露的骨头。在位于主窗口的组合框中,用户从从数据库加载的列表中选择一个幻想类(战士、盗贼、法师等)。此信息被传递到位于主窗口中的 UserControl,它使用 MVVM 和数据绑定公开有关该类的详细信息。到目前为止,所有这些都有效。

DB 有一个保存为 int 的值(在本例中为 Gear),此时在屏幕上显示为 int。将其解析为字符串是应用程序的责任。

所以问题是:我如何在 UserControl 的 ViewModel 中连接一个方法,以便在其关联的 View 发生 DataContext(选定的 CharacterClass)更改时触发?

主窗口:

<Window x:Class="ExampleApp.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:b="http://schemas.microsoft.com/xaml/behaviors"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:ExampleApp"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">

    <Window.DataContext>
        <local:MainWindowViewModel />
    </Window.DataContext>

    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="Auto"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <ComboBox Height="22" MinWidth="70" 
                  ItemsSource="{Binding Classes}" 
                  DisplayMemberPath="Name" 
                  SelectedItem="{Binding SelectedClass}"/>
        <local:DetailsView Grid.Column="1" DataContext="{Binding SelectedClass}"/>
    </Grid>
</Window>

主窗口视图模型:


namespace ExampleApp
{
    class MainWindowViewModel : Observable
    {
        private ObservableCollection<CharacterClass> _Classes;
        private CharacterClass _SelectedClass;

        public ObservableCollection<CharacterClass> Classes
        {
            get { return _Classes; }
            set { SetProperty(ref _Classes, value); }
        }
        public CharacterClass SelectedClass
        {
            get { return _SelectedClass; }
            set { SetProperty(ref _SelectedClass, value); }
        }

        public MainWindowViewModel()
        {
            LoadCharacterClasses();
        }

        private void LoadCharacterClasses()
        {
            //simulated data retrieval from a DB.
            //hardcoded for demo purposes
            Classes = new ObservableCollection<CharacterClass>
            {
                //behold: Gear is saved as an int.
                new CharacterClass { Name = "Mage", Gear = 0, Stats = "3,2,1" },
                new CharacterClass { Name = "Rogue", Gear = 1, Stats = "2,2,2" },
                new CharacterClass { Name = "Warrior", Gear = 2, Stats = "1,2,3" }
            };
        }
    }
}

我的 CharacterClass 定义。继承自封装了 INotifyPropertyChanged 的​​ Observable

namespace ExampleApp
{
    public class CharacterClass : Observable
    {
        private string _Name;
        private int _Gear;
        private string _Stats;

        public string Name
        {
            get { return _Name; }
            set { SetProperty(ref _Name, value); }
        }
        public int Gear
        {
            get { return _Gear; }
            set { SetProperty(ref _Gear, value); }
        }
        public string Stats
        {
            get { return _Stats; }
            set { SetProperty(ref _Stats, value); }
        }
    }
}

关于 Observable 基类的详细信息:

using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace ExampleApp
{
    public class Observable : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged = delegate { };

        protected virtual void OnPropertyChanged([CallerMemberName]string propertyName = null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }

        protected virtual void SetProperty<T>(ref T member, T val, [CallerMemberName] string propertyName = null)
        {
            if (object.Equals(member, val)) return;

            member = val;
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

DetailsView 用户控件:

<UserControl x:Class="ExampleApp.DetailsView"
             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:ExampleApp"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">

    <UserControl.Resources>
        <DataTemplate DataType="{x:Type local:DetailsViewModel}">
            <local:DetailsView/>
        </DataTemplate>
    </UserControl.Resources>

    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="Auto"/>
        </Grid.ColumnDefinitions>
        <StackPanel>
            <Label Content="Name:"/>
            <Label Content="Base Stats"/>
            <Label Content="Starting Gear"/>
        </StackPanel>
        <StackPanel Grid.Column="1">
            <Label Content="{Binding Name}"/>
            <Label Content="{Binding Stats}"/>
            <Label Content="{Binding gearToString}"/>
        </StackPanel>
    </Grid>
</UserControl>

最后:DetailsViewModel:

public class DetailsViewModel : Observable
    {
        public string GearToString;

        //The method I would like to have called whenever the selected
        //CharacterClass (DetailsView.DataContext, so to speak) changes.
        private void OnCharacterClassChanged(int gearNumber)
        {
            switch (gearNumber)
            {
                case 0:
                    GearToString = "Cloth";
                    break;
                case 1:
                    GearToString = "Leather";
                    break;
                case 2:
                    GearToString = "Plate";
                    break;
                default:
                    GearToString = "*Error*";
                    break;
            }
        }
    }

我一直在尝试在 DetailsView 标签更新时触发命令。 尝试将 DetailsViewModel.GearToString 转换为依赖项属性失败。 我试图在 DetailsViewModel 中覆盖 Observable 的 SetProperty。

如果我设法正确实施它们,我不知道这些尝试中的哪一个(如果有的话)是可行的(我现在才编码几个月:))

我可以使用 DetailsView 代码隐藏让它工作,但这不是 MVVM'y。

【问题讨论】:

  • 角色有int Gear,您想将其可视化为string。这可以通过使用简单的转换器来实现。另一种可能性是使用 viewmodel:你已经有 SelectedClass 属性,在它的设置器中你可以运行逻辑,这将改变另一个属性。

标签: c# wpf xaml mvvm


【解决方案1】:

因为您通过组合框更改了 DetailViews DataContext,所以您可以在组合框更改 SelectedItem之前访问“当前”DetailDataContext。 您可以在此处执行此操作:

public CharacterClass SelectedClass
{
   get { return _SelectedClass; }
   set { 
          _SelectedClass.DoWhatever();
          SetProperty(ref _SelectedClass, value); 
   }
}

或者您可以通过命令处理 ComboBoxes SelectionChanged 事件。您的旧值在 e.RemovedItem 中。

private void Selector_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
    if (e.RemovedItems.Count > 0)
        (e.RemovedItems[0] as CharacterClass).DoSomething();
}

我更喜欢这种方法,因为如果你在 setter 中放置太多逻辑,它会很快变得混乱。它会导致难以跟踪和调试的连锁反应。

通常,视图模型通过事件相互通信。在 EventAggregator、MessageBus 或类似工具的帮助下,在更复杂/断开连接的情况下。

【讨论】:

    猜你喜欢
    • 2013-10-28
    • 2021-01-19
    • 1970-01-01
    • 1970-01-01
    • 2010-11-06
    • 1970-01-01
    • 2021-03-14
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多