我创建了一个带有占位符文本和清除按钮的文本框。我已经使用数据上下文的视图模型以及目标类型为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#和引用。
答案 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
属性,并且构造函数变为无参数。