ObservesProperty method isn't observing model&

2019-01-19 08:05发布

问题:

I'm trying to learn Prism MVVM, and i'm making a window with 2 fields and a button, that gets enabled when this two fields aren't empty.

The problem is that i can't find a way to make the method ObservesProperty() work on an object (Pessoa in that case). The CanExecuteAtualizar() method only gets called at the app startup, and when i edit the textfields Nome or Sobrenome nothing happens to the button and the method isn't fired...

I tried to work without a model, putting the Nome, Sobrenome and UltimaAtualizacao properties directly in the ViewModel and it works fine, disabling the button according to the return of the method CanExecuteAtualizar, but i wanted to use it with a model instead. Is there a way to do this?

ViewAViewModel.cs

public class ViewAViewModel : BindableBase
{
    private Pessoa _pessoa;

    public Pessoa Pessoa
    {
        get { return _pessoa; }
        set { SetProperty(ref _pessoa, value); }
    }

    public ICommand CommandAtualizar { get; set; }

    public ViewAViewModel()
    {
        Pessoa = new Pessoa();
        Pessoa.Nome = "Gabriel";
        CommandAtualizar = new DelegateCommand(ExecuteAtualizar, CanExecuteAtualizar).ObservesProperty(() => Pessoa.Nome).ObservesProperty(() => Pessoa.Sobrenome);
    }

    public bool CanExecuteAtualizar()
    {
        return !string.IsNullOrWhiteSpace(Pessoa.Nome) && !string.IsNullOrWhiteSpace(Pessoa.Sobrenome);
    }

    public void ExecuteAtualizar()
    {
        Pessoa.UltimaAtualizacao = DateTime.Now;
    }
}

Pessoa.cs

public class Pessoa : BindableBase
{
    private string _nome;

    public string Nome
    {
        get { return _nome; }
        set { SetProperty(ref _nome, value); }
    }

    private string _sobrenome;

    public string Sobrenome
    {
        get { return _sobrenome; }
        set { SetProperty(ref _sobrenome, value); }
    }

    private DateTime? _ultimaAtualizacao;

    public DateTime? UltimaAtualizacao
    {
        get { return _ultimaAtualizacao; }
        set { SetProperty(ref _ultimaAtualizacao, value); }
    }
}

ViewA.xaml

<UserControl x:Class="PrismDemo.Views.ViewA"
                         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
                         xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
                         xmlns:local="clr-namespace:PrismDemo.Views"
                         mc:Ignorable="d"
                         d:DesignHeight="100" d:DesignWidth="500">
    <Grid Background="White">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="2*" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="*" />
            <RowDefinition Height="*" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <Label Content="Nome:"  Grid.Column="0" Grid.Row="0" HorizontalAlignment="Left" VerticalAlignment="Center" />
        <TextBox Grid.Column="1" Grid.Row="0"  Margin="3" TabIndex="0" Text="{Binding Pessoa.Nome, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
        <Label Content="Sobrenome:"  Grid.Column="0" Grid.Row="1" HorizontalAlignment="Left" VerticalAlignment="Center" />
        <TextBox Grid.Column="1" Grid.Row="1"  Margin="3" TabIndex="1" Text="{Binding Pessoa.Sobrenome, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
        <Label Content="Última atualização:"  Grid.Column="0" Grid.Row="2" HorizontalAlignment="Left" VerticalAlignment="Center" />
        <Label Grid.Column="1" Grid.Row="2"  Margin="3" HorizontalAlignment="Left" Content="{Binding Pessoa.UltimaAtualizacao, Mode=TwoWay}" />
        <Button Content="Atualizar" Grid.Column="1" Grid.Row="3" Width="70" Margin="2,2,3,2" HorizontalAlignment="Right" Command="{Binding CommandAtualizar}" />
    </Grid>
</UserControl>

回答1:

DelegateCommand.ObservesPropery doesn't support complex object properties. It only supports properties that exist on the ViewModel in the command is defined. This is because the lifecycle of complex objects are unknown, and a memory leak would be created if many instances of the object was created. My recommendation would be to define you property like this:

private Pessoa _pessoa;
public Pessoa Pessoa
{
    get { return _pessoa; }
    set 
    {
        if (_pessoa != null)
            _pessoa.PropertyChanged -= PropertyChanged; 

        SetProperty(ref _pessoa, value);


        if (_pessoa != null)
            _pessoa.PropertyChanged += PropertyChanged;
    }
}

Then in the PropertyChanged method, call DelegateCommand.RaiseCanExecuteChanged

EDIT: Complex property support is now available in Prism for Xamarin.Forms 7.0.



回答2:

It is true that DelegateCommand.ObservesPropery does not support complex objects, but its the way Commands are meant to be used with Prism. Manually calling PropertyChanged is an ugly hack in my opinion and should be avoided. Also it bloates the code again which Prism tries to reduce.

Moving all properties of the complex type into the ViewModel on the other hand would reduce the readability of the ViewModel. The very reason you create complex types in such scenarios is to avoid having too many single properties there in the first place.

But instead you could move the Command definition inside the complex type. Then you can set ObservesProperty for all simple properties in the constructor of the complex type and everything works as expected.

Model:

using Prism.Commands;
using Prism.Mvvm;
using static System.String;

public class LoginData : BindableBase
{
    public LoginData()
    {
        DbAddr = DbName = DbUser = DbPw = "";

        TestDbCommand = new DelegateCommand(TestDbConnection, CanTestDbConnection)
            .ObservesProperty(() => DbAddr)
            .ObservesProperty(() => DbName)
            .ObservesProperty(() => DbUser)
            .ObservesProperty(() => DbPw);
    }

    public DelegateCommand TestDbCommand { get; set; }

    public bool CanTestDbConnection()
    {
        return !IsNullOrWhiteSpace(DbAddr)
            && !IsNullOrWhiteSpace(DbName)
            && !IsNullOrWhiteSpace(DbUser)
            && !IsNullOrWhiteSpace(DbPw);
    }

    public void TestDbConnection()
    {
        var t = new Thread(delegate () {
            Status = DatabaseFunctions.TestDbConnection(this);
        });
        t.Start();
    }

    private string _dbAddr;
    public string DbAddr
    {
        get => _dbAddr;
        set => SetProperty(ref _dbAddr, value);
    }

    ...
}

ViewModel:

public class DatabaseConfigurationViewModel
{
    public DatabaseConfigurationViewModel()
    {
        CurrentLoginData = new LoginData(true);
    }

    public LoginData CurrentLoginData { get; set; }
}

View:

<UserControl x:Class="TestApp.Views.DatabaseConfiguration"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
         xmlns:prism="http://prismlibrary.com/"
         prism:ViewModelLocator.AutoWireViewModel="True"
         mc:Ignorable="d" 
         d:DesignHeight="300" d:DesignWidth="300">
<Grid>
    <StackPanel Orientation="Vertical">
        <Label>IP Adresse oder URL:</Label>
        <TextBox Text="{Binding CurrentLoginData.DbAddr, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"></TextBox>
        ...
        <Button Command="{Binding CurrentLoginData.TestDbCommand}">Teste Verbindung</Button>
       </StackPanel>
   </Grid>



回答3:

In the Prism version that I have (7.0.0.362), you can use ObserveCanExecute, and pass property HasChanges that is updated on every property change of your entity.

TestDbCommand = new DelegateCommand(TestDbConnection).ObservesCanExecute(() => HasChanged);
Pessoa = new Pessoa();
Pessoa.PropertyChanged += Pessoa_PropertyChanged;

Then update HasChanges with a validation method in the constructor, and detach the method in the destructor.

private void Pessoa_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
    HasChanged = ValidatePessoa(Pessoa);
}

~YourViewModel()
{
    Pessoa.PropertyChanged -= Pessoa_PropertyChanged;
}
bool _hasChanged;
public bool HasChanged { get => _hasChanged; set => SetProperty(ref _hasChanged, value); }


标签: wpf mvvm Prism