Как правильно передать реф на вьюмодель - PullRequest
0 голосов
/ 11 июля 2020

Я создал текстовое поле с текстом-заполнителем и кнопкой очистки. Я реализовал его, используя модель представления для контекста данных и стиль с целевым типом 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);

Определенно не кажется правильным, что мне нужно передать явный установщик (changeHandler) в PlaceholderTextBoxViewModel. Кажется, действительно, что ref, который я передаю, на самом деле никогда не используется (и необходим вообще - хотя и не в качестве ссылки - если в поле должен быть уже существующий текст).

Я никогда раньше не использовал refs и, должно быть, что-то делаю не так. Я также попытался указать все, что использует свойство Name (в последнем фрагменте кода), в поле _name, но это не работает, поле не обновляется должным образом или, по крайней мере, не «взаимодействует» его обновления (в различных случаях CanExecute не обновляются, SearchPredicate не обновляются и т. д. c). Я использую MVVMLight и полагаю, что изменение значения поля не вызывает OnPropertyChanged - если значение поля вообще меняется.

Как мне заставить ссылку работать правильно? Я делаю это полностью неправильно?

Я понимаю, что есть другие способы реализовать это TextBox с его командой clear, даже в чистом MVVM (а именно, если я поставлю ClearCommand в потребляющей виртуальной машине вместо самой виртуальной машины текстового поля, тогда для текстового поля вообще не требуется виртуальная машина). Но мне бы очень хотелось знать, как разобраться в моей попытке решения, хотя бы для лучшего понимания C# и ссылок.

Ответы [ 3 ]

0 голосов
/ 11 июля 2020

Значение 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.

Оставьте это разработчикам пользовательского интерфейса, которые используют элементы управления и как они будут реализовывать очистку журнала управления c.

Здесь пример

  • ViewModel

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

    <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;
        }
    }
    
0 голосов
/ 11 июля 2020

Самым простым решением было бы исправить вашу привязку. Вам не нужно вручную пересылать данные, пытаясь реализовать свою собственную систему уведомлений об изменениях.

Просто убедитесь, что ваш источник данных всегда реализует 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, и конструктор становится без параметров.

0 голосов
/ 11 июля 2020

Проблема здесь, похоже, архитектурная. MVVM - это многоуровневая архитектура, которая выглядит следующим образом:

Model -> View Model -> View

Модель - это самый низкий уровень, просмотр - самый высокий. Что еще более важно, каждый уровень не имеет прямой видимости ни на один из уровней над ним.

Проблема в вашем примере заключается в том, что вы нарушили разделение ответственности. У вас есть родительский класс, создающий PlaceholderTextBoxViewModel, что подразумевает его в модели представления. Однако этот же класс содержит свойство «Имя», которое на самом деле является данными, которые должны находиться на уровне вашей модели. Ваша существующая архитектура такова, что ваша модель представления не может видеть уровень данных, поэтому вам фактически пришлось настроить собственный механизм уведомления об изменении свойств, используя ссылку и делегат, чтобы ваша модель и модель представления были синхронизированы.

Выбросьте все и начните заново. Ваша модель должна содержать 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 ....
}

Я придерживался вашей номенклатуры использования «Name» в модели и «Text» в модели представления, на практике они обычно одинаковы, но решать вам. В любом случае, у вас все еще есть уведомление об изменении свойства в вашей модели представления, и это слой модели представления обновляет уровень модели.

Очевидно, есть много вариантов этого. Если вы не хотите, чтобы изменения сразу же распространялись на уровень вашей модели (а есть много случаев, когда вам это может не понадобиться), укажите текстовое поле поддержки (_Text) и выполняйте синхронизацию только в тех точках, которые вы хотите. происходят. И, конечно, если вы хотите go сделать еще один шаг, тогда классы вашей модели могут вместо этого реализовывать интерфейсы, и вы можете использовать внедрение зависимостей для внедрения этих интерфейсов в классы модели представления вместо предоставления им доступа к самим фактическим реализациям.

Прежде всего, имейте в виду, что единственная цель модели представления - подготовить данные уровня модели для использования представлением. Что-нибудь еще ... данные, домен, бизнес-логики c и c ... все это относится к уровню вашей модели, который вообще не должен иметь видимости на уровне модели представления.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...