5

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>
Gabriel Duarte
  • 974
  • 1
  • 13
  • 28

3 Answers3

7

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.

  • @Brian Have you tried it? This approach doesn't seem working for me. – mechanic Aug 16 '16 at 22:44
  • @mechanic Yes, this is the approach I use for this scenario. Your properties in your model must call PropertyChanged for this to work. –  Aug 17 '16 at 00:45
  • @Brian You're right, I forgot to implement INPC for my "model" properties. BTW why do you do this double `_pessoa != null` check and `-=`,`+=` in your setter? Why just don't you call `RaiseCanExecuteChanged()` after `SetProperty`? – mechanic Aug 17 '16 at 14:26
  • SetProperty does call PropertyChanged for the property in the VM, not for sub properties of the instance. The event is needed to respond to property changes in the model instance properties, not VM properties. –  Aug 17 '16 at 17:56
  • The ObservesProperty implementation is 'suboptimal'. If the expression parameter doesn't admit sub-properties it should raise an exception and tell straight away that it doesn't work and not accepting any kind of Expression. – Paulo Jan 20 '18 at 00:05
  • @Paulo we accept PRs if you think you can improve it. –  Jan 20 '18 at 00:21
  • @Paulo doesn't matter now since this is supported in the latest version of Prism. –  Jan 20 '18 at 00:24
  • Thanks for the update @BrianLagunas , I've upgraded Prism.Core version 7.0.0.396 (wpf), but still don't see it notifying when the expression has an access member, for example new DelegateCommand(OnAddStaff, CanAddStaff) .ObservesProperty(() => NewStaffDto.RoleId) – Paulo Jan 23 '18 at 00:31
  • You can't update just WPF to v7 as there are breaking changes and no binary compatibility. There is no release of v7 for WPF yet –  Jan 23 '18 at 02:15
  • Thanks again @BrianLagunas, I hope these goodies come over also to WPF or, in the case that there is serious breaking changes, create a new Prism release product. With WPF the command binding refreshing cycle has resolved this with the static command manager at the cost of refreshing all the bindings. It wasn't posible to specify the order of refresh though, But it works. In the meantime. I think it is less complicated to use ObserveCanExecute, as in the answer I've put below. – Paulo Jan 23 '18 at 08:38
1

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>

RonnyR
  • 210
  • 1
  • 5
  • You could do that, but i wouldn't recommend it simply because that way you would be mixing view related stuff with the class model. Because of that, you would not be able to use `LoginData` within any other view when `CanTestDbConnection` has different rules. For that case, you would have to create another class with the same properties, breaking the purpose of a creating a model. – Gabriel Duarte Oct 15 '17 at 03:21
0

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); }
Paulo
  • 172
  • 1
  • 5