Autowire CanSave and Save

Topics: Conventions, Getting Started
Jun 24, 2011 at 5:10 PM

Hello,

I am having a bit of an issue with the conventions auto wiring up to public property on my view model.  The below code is based on the samples Caliburn.Micro.Hello - where there was a text box and button the the SayHello command all wired up.  For my scenario, I have a class that is my public property, not string.  This is a scaled down simple example to make it easier to see what is going on.

First question is do I have it wired up correctly?  Using d:DataContext="{Binding Source={StaticResource ShellViewModel}}" and bind it by using Text="{Binding Result.ResultValue, Mode=TwoWay, ValidatesOnDataErrors=true}"  This does display the data correctly, but I am not sure this is the best way.

Second question is how can I get the CanSaveResult working while I type in the ResultValue textbox?  Ultimately my CaseResult class will have several strings that are required so I want to use the IsValid method.

 

XAML

<UserControl x:Class="Caliburn.Micro.Hello.ShellView"
             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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             mc:Ignorable="d" 
             d:DataContext="{Binding Source={StaticResource ShellViewModel}}">
    <StackPanel>
        <TextBox x:Name="Name" />
        <Button x:Name="SayHello"
                Content="Click Me" />

        <TextBox x:Name="ResultValue" Text="{Binding Result.ResultValue, Mode=TwoWay, ValidatesOnDataErrors=true}" />
        <Button x:Name="SaveResult"
                Content="Save Result" />
    </StackPanel>
</UserControl>

View Model

CaseResult m_Result;
public CaseResult Result
{
    get { return m_Result; }
    set
    {
        m_Result = value;
        NotifyOfPropertyChange(() => Result);
        NotifyOfPropertyChange(() => CanSaveResult);
    }
}

public bool CanSaveResult
{
    get { return Result.IsValid; }
}

public void SaveResult()
{
    MessageBox.Show(string.Format("Hello {0}!", Result.ResultValue)); //Don't do this in real life :)
}  

CaseResult Class

public class CaseResult
{
    private event PropertyChangedEventHandler m_propertyChanged;

    private string m_resultValue = string.Empty;
    public string ResultValue
    {
        get
        {
            return m_resultValue;
        }
        set
        {
            if (!object.Equals(m_resultValue, value))
            {
                m_resultValue = value;
                OnPropertyChanged(new PropertyChangedEventArgs("ResultValue"));
                IsDirty = true;
                if (!string.IsNullOrEmpty(m_resultValue))
                {
                    m_isValid = true;
                }
            }
        }
    }

    private bool m_isDirty = false;
    public bool IsDirty
    {
        get
        {
            return m_isDirty;
        }
        set
        {
            m_isDirty = value;
        }
    }

    private bool m_isValid = false;
    public bool IsValid
    {
        get
        {
            return m_isValid;
        }
        set
        {
            m_isValid = value;
        }
    }


    protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
    {
        if (!object.Equals(m_propertyChanged, null))
        {
            m_propertyChanged(this, e);
        }
    }
}

Jun 24, 2011 at 10:43 PM

first thought is this...

public class CaseResult : PropertyChangedBase{ 
//etc...
}

which is exactly like public class CaseResult : INotifyPropertyChanged {} without the extraneous extra OnPropertyChanged(xxx) code that is necessary in the class.

This is an inherited base class already available in CM. Which gives you acces to NotifyOfPropertyChanged()  which can take a string of the property name or NotifyOfPropertyChanged(()=>IsDirty); The later is just cleaner.   

d:DataContext="{Binding Source={StaticResource ShellViewModel}}"  is for test data only. Fit & Finish for designers for the most part.  Since it's an ignored property, based on the declaration.

 

 

Jun 25, 2011 at 12:06 AM

Thanks for the reply.  I tried your suggestions for inheriting PropertyChangedBase but still came up with the same results.  As the code is in my original post, the setting of ResultValue gets called when losing focus of the ResultValue textbox, but even then the button is never enabled.  I would like it to occur on change just like the SayHello button does in the example.  I am pretty new to MVVM and especially CM, so bear with me.

 

Jun 25, 2011 at 7:18 AM
Edited Jun 25, 2011 at 7:21 AM

Ok so your viewmodel is getting wired up to the View right?

For getting an update as you type.... It would UpdateSourceTrigger=PropertyChange.  The current config is for lostfocus.  The other of the 3 triggers is Explicit (lostfocus, propertychanged, explicit).

Text="{Binding Result.ResultValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=true}"

Jun 25, 2011 at 10:10 AM
Edited Jun 25, 2011 at 10:10 AM

Hi LowTide,

I see several things that could prevent your scenario to work, but I also feel I could be missing your actual intention, so please correct me if I've missed something.
Let's point out some of this potential problems:

1. as mvermef note, you are using d:DataContext, which is intended to provide a DC in design scenarios only.
In order to provide a DC at runtime you should use DataContext property (with no "d:" prefix). 

2. the previous point should fix V/VM binding in *plain* WPF/SL scenarios, but doesn't instruct Caliburn.Micro to apply conventions.
In order to conventional binding wired, you have to use cal:Bind.Model attached property, which instructs CM to take care of applying convention just before setting the DataContext:

cal:Model.Bind="{Binding Source={StaticResource ShellViewModel}}"

Also, with conventions in place, you can modify the textbox like this: 

<TextBox x:Name="Result_ResultValue" />  (note the "_" in name)

having it bound as intended.

3. The approach of "pulling" the VM instance from the View (called "View-First") significantly differs from the one used in almost all CM samples.
The most frequently used approach in CM is called "Model-First", and consists in creating the VM on the first place, then let the framework locate the corresponding View for the VM instance.
For example, in the CM.Hello sample, the View has no explicit reference to a VM and no DataSource or cal:Model.Bind setting.
The ShellVM is created by the bootstrapper, and the corresponding ShellView is afterwards located, instantiated and bound to VM by the infrastructure.

I'm not saying you should be using Model-First (since CM supports View-First as well, with cal:Model.Bind); I just wanted to point out the difference to help you understand the samples and choose the approach that best fits your needs.

4. CanSaveResult is never updated in the UI because it only has a single change notification, perhaps in the ShellVM constructor, when you set Result property.
When you change CaseResult.ResultValue, CaseResult notifies for it, but there's nothing notifying ShellVM.CanSaveResult in turn.
A tecnique I often use is to subscribe CaseResult.PropertyChangedEvent in the ShellVM, then notify for CanSaveResult each time the property IsValid gets changed (make sure to notify for IsValid too!)
I know that all this INotifyPropertyChanged stuff is a little confusing at first, but it gets more evident as you get used to it.

Hope it helps.

Jun 27, 2011 at 7:48 PM
mvermef wrote:

For getting an update as you type.... It would UpdateSourceTrigger=PropertyChange.  The current config is for lostfocus.  The other of the 3 triggers is Explicit (lostfocus, propertychanged, explicit).

Text="{Binding Result.ResultValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=true}"

Thanks for the reply.  I tried going this route, but PropertyChanged is not available - Only choices where LostFocus and Explicit.   I tried forcing it in there, but it would just crash at run time.

Jun 27, 2011 at 7:52 PM
marcoamendola wrote:

4. CanSaveResult is never updated in the UI because it only has a single change notification, perhaps in the ShellVM constructor, when you set Result property.

When you change CaseResult.ResultValue, CaseResult notifies for it, but there's nothing notifying ShellVM.CanSaveResult in turn.
A tecnique I often use is to subscribe CaseResult.PropertyChangedEvent in the ShellVM, then notify for CanSaveResult each time the property IsValid gets changed (make sure to notify for IsValid too!)
I know that all this INotifyPropertyChanged stuff is a little confusing at first, but it gets more evident as you get used to it.

Hi mvermef,  I basically went with your #4.  I started this time with the Caliburn.Micro.HelloMef version of the project and did as you said, subscribe to the PropertyChangedEvent and that did the trick.    Also, the binding for Result_ResultValue worked like a champ in the VM first approach with no extra binding code in the top of the XAML.

        public ShellViewModel()
        {
            this.m_Result.PropertyChanged += new PropertyChangedEventHandler(m_Result_PropertyChanged);
        }

        void m_Result_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            NotifyOfPropertyChange(() => CanSaveResult);
        }

The reason I went with the most simple solution originally, because I was trying to proved the scenario with the smallest footprint possible that illustrated my issue.  We are actually going the route of VM first on our project.

Thanks again everyone for the help here.