0
votes

I use the following NEW BindableViewModel class(Old one I also post at the bottom of this topic) to make a object observable(from MS sample):

public abstract class BindableBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged = delegate { };

    protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
    {
        if (object.Equals(storage, value)) return false;

        storage = value;
        this.OnPropertyChanged(propertyName);
        return true;
    }

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

I want use this to create a ViewModel in order to bind those TextBox Controls, such as this:

public class TextBoxModel : BindableBase
{
    private string _text = string.Empty;
    // I'm going to bind this field to TextBox.Text property
    public string Text { get { return _text; } set { SetProperty(ref _text, value); } }

    private bool _isEnabled = true;
    // I'm going to bind this field to TextBox.IsEnabled property
    public bool IsEnabled { get { return _isEnabled; } set { SetProperty(ref _isEnabled, value); } }
}

Then, in my Page ViewModel, I use TextBoxModel to define fields:

public class Page1ViewModel : BindableBase
{
    private TextBoxModel _firstName = null;
    public TextBoxModel FirstName { get { return _firstName; } set { 
        if (SetProperty(ref _firstName, value))
            {
                SubmitCommand.RaiseCanExecuteChanged();
            } } }

    private TextBoxModel _surname = null;
    public TextBoxModel Surname { get { return _surname; } set { 
        if (SetProperty(ref _surname, value))
            {
                SubmitCommand.RaiseCanExecuteChanged();
            } } }

    private DelegateCommand _submitCommand = null;
    public DelegateCommand SubmitCommand { get { return _submitCommand??(_submitCommand=new DelegateCommand(SubmitExecute, SubmitCanExecute)); } }
    private void SubmitExecute()
    {
        MessageBox.Show($"{FirstName.Text} {Surname.Text}");
    }
    private bool SubmitCanExecute()
    {
        if(string.IsNullOrEmpty(FirstName.Text))
            return false;
        else if(string.IsNullOrEmpty(Surname.Text))
            return false;
        else
            return true;
    }
}

In my XAML View, I set textbox binding as usual:

<TextBox Text="{Binding FirstName.Text, UpdateSourceTrigger=PropertyChanged}" IsEnabled="{Binding FirstName.IsEnabled}"/>
<TextBox Text="{Binding Surname.Text, UpdateSourceTrigger=PropertyChanged}" IsEnabled="{Binding Surname.IsEnabled}"/>
<Button Content="Submit" Command{Binding SubmitCommand} />

When I run this, I've found text changing is not work. It didn't trigger if(SetProperty(ref _firstName, value)) or if(SetProperty(ref _surname, value)) in the setter. If I don't combine the properties in a TextBoxModel, everything works fine. If I use OLD ViewModelBase, TextBoxModel worked fine, too.

So I think I must missed something when using the NEW BindableBase class? Looking for your help, thanks!

OLD ViewModelBase:

[Obsolete("Please use BindableBase。")]
public abstract class ObservableModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public static string GetPropertyName<T>(System.Linq.Expressions.Expression<Func<T>> e)
    {
        var member = (System.Linq.Expressions.MemberExpression)e.Body;
        return member.Member.Name;
    }

    protected virtual void RaisePropertyChanged<T>
        (System.Linq.Expressions.Expression<Func<T>> propertyExpression)
    {
        RaisePropertyChanged(GetPropertyName(propertyExpression));
    }

    protected void RaisePropertyChanged(String propertyName)
    {
        System.ComponentModel.PropertyChangedEventHandler temp = PropertyChanged;
        if (temp != null)
        {
            temp(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName));
        }
    }
}
2
I don't see how this could work. You're not setting FirstName or Surname when the text changes (or, in fact, at all!), so SetProperty won't be called. You need to observe the property changes within TextBoxModel.Charles Mager
@CharlesMager what do you mean? I do changed the Text property of FirstName object, but it didn't trigger if(SetProperty(ref _firstName, value)). But, it triggered Text property inside of FirstName object. Why didn't trigger if(SetProperty(ref _firstName, value)) even Text property inside of it triggered?Oh My Dog
Your binding is to the Text property, which is a string. Trying to set that value will not magically new up a TextBoxModel and set FirstName as well. It will just silently fail because FirstName is null.Charles Mager

2 Answers

1
votes

Your binding will never set FirstName or LastName, and nothing else you've shown does either. These will both be null. A binding to Text will only set that value, it won't create a new TextBoxModel and set that.

If you want this to work while organised this way, you'll need to do something like this:

public class Page1ViewModel : BindableBase
{
    private readonly TextBoxModel _firstName = new TextBoxModel();

    public Page1ViewModel()
    {
        _firstName.PropertyChanged += 
            (sender, args) => SubmitCommand.RaiseCanExecuteChanged();
    }

    public TextBoxModel FirstName 
    {
        get { return _firstName; }
    }
}
1
votes

What you trying to do makes sense, but how you are doing it is needlessly complicated. You have a TextBoxModel which is trying to mimic a real TextBox which you simply don't need to do, I think your approach is all wrong.

Firstly, the Model represents data, not UI. Instead of creating a TextBoxModel that represents a TextBox, you should instead create a model that represents your data structure, such as a PersonModel, or a UserModel. Here is an example of what I mean:

public class PersonModel
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

Note: You could put INotifyPropertyChanged stuff in the Model, but that isn't really a UI concern, it's just data. The best place for the property changed implementation is in the View Model, which I'll explain further down.

Ok, now the data layer is sorted, you simply need to expose this model to the View via the View Model. Firstly, here is a simple property changed base that I will use for this example:

public abstract class PropertyChangedBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public void NotifyOfPropertyChange([CallerMemberName]string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

Expose the model properties like this:

public class PersonViewModel : PropertyChangedBase
{
    private PersonModel _Person;

    public string FirstName
    {
        get { return _Person.FirstName; }
        set
        {
            _Person.FirstName = value;
            NotifyOfPropertyChange();
        }
    }

    public string LastName
    {
        get { return _Person.LastName; }
        set
        {
            _Person.LastName = value;
            NotifyOfPropertyChange();
        }
    }

    //TODO: Your command goes here

    public PersonViewModel()
    {
        //TODO: Get your model from somewhere.
        _Person = new PersonModel();
    }
}

Now you can simply bind your TextBox to the FirstName or LastName view model property:

<TextBox Text="{Binding FirstName}" ... />

It is really as simple as that. You do not need to recreate a TextBox in your data layer, in fact all UI concerns should be completely separate from the model layer.

Using this method, you also do not need to worry about raising the CanExecuteChanged method because it's already handled by INotifyPropertyChanged.

Remember, UI is UI, Data is Data.