简体   繁体   中英

Update a Textblock in ViewModel from another Class

I have created an interface (ViewModel) for my application to control my elements. I want to set a text for a Textblock in a class.

ViewModel:

public class Model
{
    public string Username { get; set; }
}

internal class ViewModel : INotifyPropertyChanged
{
    private Model _model;
    public ViewModel()
    {
     _model = new Model();
    }
    
    string username;
    public string Username { 
        get => username; 
        set
        {
            if(username != value)
            {
                username = value;
                this.RaisePropertyChanged(nameof(_model.Username));
                
            }
        } 
    }

    

    private void RaisePropertyChanged([CallerMemberName] string name = "")
    {
        this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(name)));
    }
    public event PropertyChangedEventHandler? PropertyChanged;
}

MyClass:

Model Model = new Model();
        Model.Username = "name";

when i set the Username in public ViewModel() { Username = "test"; } public ViewModel() { Username = "test"; } It works.

XMAL:

<Page.DataContext>
    <local1:ViewModel/>
</Page.DataContext>

<TextBlock x:FieldModifier="public"  Text="{Binding Username}"/>

You must raise the PropertyChanged for the property that is the member of binding source. When you bind to a class ViewModel , you must raise the event for the property on this object and not on any unrelated objects like your class Model .

Since you don't want to bind directly to classes of the model, you must delegate the properties or write the values back to the model at the proper point in time eg after invocation of a save command.

If the model can change data independently and your view model needs to know about these changes, then the view model must observe the model. To enable observation, the model must expose corresponding events.

To delegate the property data received from the view to the model, implement property delegation:

// Update the model immediately after e.g., the data binding has changed the property
public string Username 
{ 
  get => this.Model.Username; 
  set
  {
    if (this.Model.Username != value)
    {
      this.Model.Username = value;
      RaisePropertyChanged();                
    }
  } 
}

To observe data changes in a class of the model, introduce a corresponding event. This event can notify about particular changes eg, a particular property like UsernameChanged , or it can be more general and notify about certain state changes like UserDataChaged or DataChanged :

ValueChangedEventArgs.cs

public class ValueChangedEventArgs<TValue> : EventArgs
{
  public ValueChangedEventArgs(TValue oldValue, TValue newValue)
  {
    this.OldValue = oldValue;
    this.NewValue = newValue;
  }

  public TValue NewValue { get; }
  public TValue OldValue { get; }
}

Model.cs

public class Model
{
  protected virtual void OnUsernameChanged(string oldValue, string newValue)
    => this.UsernameChanged?.Invoke(this, new ValueChangedEventArgs<string>(oldValue, newValue));

  public event EventHandler<ValueChangedEventArgs<string>> UsernameChanged;

  private string username;
  public string Username 
  { 
    get => this.username; 
    set
    {
      if (this.Username != value)
      {
        string oldValue = this.Username;
        this.username = value;
        OnUsernameChanged(oldValue, this.Username);                
      }
    } 
  }
}

ViewModel.cs

public class ViewModel : INotifyPropertyChanged
{
  public ViewModel()
  {
    this.Model = new Model();
    this.Model.UsernameChanged += OnModelUsernameChanged;
  }

  private void OnModelUsernameChanged(object sender, ValueChangedEventArgs<string> e)
  {
    if (e.NewValue != this.Username)
    {
      RaisePropertyChanged(nameof(this.Username));      
    }
  }  

  // Update the model immediately after e.g., the data binding has changed the property
  public string Username 
  { 
    get => this.Model.Username; 
    set
    {
      if (this.Model.Username != value)
      {
        this.Model.Username = value;
        RaisePropertyChanged();                
      }
    } 
  }

  private Model Model { get; }
}

Consider to hide details of the model class to add more robustness in terms of modifications to the model's implementation. For example, don't expose the properties of a model class directly. Rather build its API around a set of methods that allow to modify it. You could introduce a SaveUser(User): void method, where User is the class to encapsulate user related data (to reduce the parameter count). Now, instead of having the view model class to know which properties and methods it has to use in order to update a user, it simply invokes the SaveUser() method. The view model must not know about the internals of the model.


To fix the update issue related to two different instances of Model , you must initialize all depending classes (the ViewModel class and the MyClass class) with the same Model instance (shared instance).
Since we have to manually instantiate the ViewModel in order to pass the Model instance to the constructor, you must remove the ViewModel instantiation and DataContext assignment from the XAML of MainWIndow.xaml (and every other XAML where it is used as DataContext too):

MainWindow.xaml.cs

public partial class MainWindow : Window
{
   
    public MainWindow()
    {
        InitializeComponent();

        var sharedModelInstance = new Model();
 
        // Instantiate the ViewModel using the shared Model instance and assign it to the DataContext
        var viewModel = new ViewModel(sharedModelInstance);
        this.DataContext = viewModel;

        // Initialize MyClass using the shared Model
        MyClass mc = new MyClass(sharedModelInstance);        
        mc.UserData();
    }
}

ViewModel.cs
Replace the constructor of the ViewModel example from above with this one:

public ViewModel(Model model) 
{
  this.Model = model;
  this.Model.UsernameChanged += OnModelUsernameChanged;
}

MyClass.cs
Modify the constructor of MyClass to accept a Model instance which MyClass then uses (instead of creating its own instance):

private Model UserModel { get; }
public MyClass(Model model)
{
  this.Model = model;
}

Now, that the ViewModel is assigned to the DataContext in the constructor of MainWindow , you probably miss the design time Intellisense support. To improve the experience, you must define a design time instance of ViewModel :

<Window x:Class="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"

        d:DataContext="{d:DesignInstance Type=local:ViewModel}">
</Window>

your class should look like this:
(you want to use different name for ModelField probably)

public class Model : INotifyPropertyChanged
{
    string username;
    public string Username { 
        get => username; 
        set
        {
            if(username != value)
            {
                username = value;
                this.RaisePropertyChanged(nameof(Username));
                
            }
        } 

... }

internal class ViewModel : INotifyPropertyChanged
{
    private Model _model;
    public ViewModel()
    {
     _model = new Model();
    }
    

    public Model ModelField { 
        get => _model; 
        set
        {
            if(_model != value)
            {
                _model = value;
                this.RaisePropertyChanged(nameof(ModelField));
                
            }
        } 
    }
...
}

and the xaml should bind to ModelField.UserName

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM