如何将参考正确传递给视图模型

时间:2020-07-10 23:50:04

标签: c# wpf

我创建了一个带有占位符文本和清除按钮的文本框。我已经使用数据上下文的视图模型以及目标类型为TextBox的样式来实现了它。在xaml中,使用它非常简单。

<TextBox DataContext="{Binding NameBox}" Style="{StaticResource placeholder}"/>

不过,我实现视图模型的方式对我来说很有趣:

public class PlaceholderTextBoxViewModel : NotifiableViewModelBase {
    private string text;

    public string Text {
        get => text;
        set {
            text = value;
            OnTextChange(text);
        }
    }

    public string PlaceholderText { get; set; }

    public RelayCommand ClearCommand => new RelayCommand(() => Text = "");

    private event Action<string> OnTextChange;

    public PlaceholderTextBoxViewModel(ref string text, string placeholderText, Action<string> changeHandler = null) {
        OnTextChange = changeHandler ?? (_ => { });
        Text = text;
        PlaceholderText = placeholderText;
    }
}

万一感觉还不错,请查看其用法

private string _name;
public string Name {
    get => _name;
    set {
        _name = value;
        System.Console.WriteLine(_name); // needed to silence auto prop error
    }
}

public PlaceholderTextBoxViewModel NameBox { get; }

// in the constructor...
NameBox = new PlaceholderTextBoxViewModel(ref _name, "Exam Name", t => Name = t);

我显然需要将显式的setter(changeHandler)传递给PlaceholderTextBoxViewModel似乎并不正确。的确,我所传递的ref似乎从未真正使用过(并且仅在必要时才使用-尽管不是作为参考-如果框中有预先存在的文本)。

我以前从未使用过引用,所以我一定做错了。我还尝试将使用Name属性(在最终代码摘录中)的所有内容都指向_name字段,但这行不通,该字段未正确更新,或者至少不行。 t“传达”其更新(在各种用途中,CanExecute未更新,SearchPredicate未刷新,等等)。我正在使用MVVMLight,并且我想更改字段的值不会触发OnPropertyChanged -即使该字段的值根本没有更改。

如何使裁判正常工作?我是完全完全做错了吗?

我了解到,即使在纯MVVM中,也有其他方法可以通过其清晰的命令来实现此TextBox(即,如果我将ClearCommand放在使用 的VM中而不是文本框的VM本身,则文本框完全不需要VM。但是我真的很想知道如何理解我尝试的解决方案,即使只是为了更好地理解C#和引用。

3 个答案:

答案 0 :(得分:0)

这里的问题似乎是一个体系结构的问题。 MVVM是一个如下所示的分层架构:

Model -> View Model -> View

模型是最低层,视图是最高层。更重要的是,每个层都无法直接看到其上方的任何层。

您的示例中的问题在于您已经打破了关注点分离。您有一个父类创建PlaceholderTextBoxViewModel,这意味着它在视图模型中。但是,同一类包含一个“名称”属性,实际上是应该在模型层中的数据。您现有的架构使得视图模型无法看到数据层,因此您必须有效地使用ref和委托建立自己的属性更改通知机制,以使模型和视图模型保持同步。

全部删除,然后重新开始。您的模型应该包含POCO,所以从这样的事情开始:

public class MyModel
{
    public string Name {get; set;}
}

然后,当您创建视图模型时,将该类的实例传递到其构造函数中,以使其具有可见性:

public class MyViewModel : NotifiableViewModelBase
{
    private MyModel Model;
    
    public MyViewModel(MyModel model) => this.Model = model;

    public string Text
    {
        get => this.Model.Name;
        set
        {
            this.Model.Name = value;
            RaisePropertyChanged(() => this.Text);
        }
    }
    // ... etc ....
}

我坚持使用在模型中使用“名称”和在视图模型中使用“文本”的术语,实际上它们通常是相同的,但这取决于您。无论哪种方式,您的视图模型中仍然会有属性更改通知,这是视图模型层更新了模型层。

显然,这有很多变化。如果您不希望更改立即传播到模型层(在很多情况下,您可能不希望这样做),请为Text提供一个后备字段(_Text),并仅在您想要的点进行同步发生。当然,如果您想更进一步,那么您的模型类可以代替实现接口,并且可以使用依赖注入将这些接口注入视图模型类,而不是让它们自己访问实际的实现。

首先,请记住,视图模型的唯一目的是准备模型层数据以供视图使用。数据,域,业务逻辑等所有其他内容都属于您的模型层,根本不应该对视图模型层有任何了解。

答案 1 :(得分:0)

ref仅在您要更改值时才有意义。

在这种情况下,您可以使用OnTextChange事件。

public PlaceholderTextBoxViewModel(ref string text, string placeholderText, Action<string> changeHandler = null) 
{
    OnTextChange = newValue => 
        {
            text = newValue; // <- value back to the ref
            changeHandler?.Invoke(newValue);
        }
    Text = text;
    PlaceholderText = placeholderText;
}

顺便说一句,您的解决方案太复杂了。保持ViewModel尽可能简单和抽象。在这种情况下,一个简单的Name属性就足够了。

留给UI开发人员使用哪些控件以及如何实现清晰的控件逻辑。

这里有个例子

  • ViewModel

    using ReactiveUI;
    using ReactiveUI.Fody.Helpers;
    
    namespace WpfApp1
    {
        public class MainViewModel : ReactiveObject
        {
            [Reactive] public string Name { get; set; }
        }
    }
    
  • 查看

    <Window
          x:Class="WpfApp1.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:local="clr-namespace:WpfApp1"
          xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
          Title="MainWindow"
          Width="800"
          Height="450"
          mc:Ignorable="d">
        <Window.DataContext>
            <local:MainViewModel />
        </Window.DataContext>
        <Grid>
            <StackPanel
                  Width="200"
                  VerticalAlignment="Center">
                <Label Content="{Binding Name}" />
                <DockPanel>
                    <Button 
                      DockPanel.Dock="Right" 
                      Content="x" 
                      Width="20" 
                      Click="NameTextBoxClearButton_Click"/>
                    <TextBox x:Name="NameTextBox"
                      Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
                      TextWrapping="Wrap" />
                </DockPanel>
            </StackPanel>
        </Grid>
    </Window>
    
  • 查看背后的代码

    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
    
        private void NameTextBoxClearButton_Click( object sender, RoutedEventArgs e )
        {
            NameTextBox.Text = string.Empty;
        }
    }
    

答案 2 :(得分:0)

最简单的解决方案是更正绑定。您不必通过尝试实现自己的变更通知系统来手动转发数据。

只需确保您的数据源始终实施INotifyPropertyChanged,并从每个属性集方法正确引发INotifyPropertyChanged.PropertyChanged事件。然后直接绑定到此属性:

class PlaceholderTextBoxViewModel : INotifyPropertyChanged
{
  private string text;
  public string Text 
  {
    get => this.text;
    set 
    {
      this.text = value;
      OnPropertyChanged();
    }
  }

  public event PropertyChangedEventHandler PropertyChanged;
  protected virtual OnPropertyChanged([CallerMemberName] string propertyName = null)
  {
    this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
  }
}

绑定语法允许引用嵌套属性。
在您的特殊情况下,{Binding}表达式必须为:

<TextBox DataContext="{Binding NameBox.Text}" />

现在,TextBox.Text值将自动传播到PlaceholderTextBoxViewModel.Text属性,并且构造函数变为无参数。

相关问题