简体   繁体   中英

How to prevent PropertyChanged from firing while controls are being initialized

This problem has been causing headaches for a while, and it is preventing the project from going forward. Consider a WPF XAML form with controls bound to a ViewModel. (I'm using the Caliburn.Micro MVVM framework and Entity Framework for the data). An Initialize() method is called by the shell to load the form's data from the database and set up PropertyChanged event handlers. There is an IsDirty flag that tracks whether there is changed data in the form. There is a "Save" button bound to the IsDirty property so that it is enabled when the data has changed.

// Sample code; forms have many controls....

// this is the property that the controls are bound to
public Entity BoundData { get; set; }

public void Initialize()
{
    // this is an example line where I query the database from the Entity Framework ObjectContext...
    BoundData = objectContext.DataTable.Where(entity => entity.ID == 1).SingleOrDefault();

    // this is to cause the form bindings to retrieve data from the BoundData entity
    NotifyOfPropertyChange("BoundData");

    // wire up the PropertyChanged event handler
    BoundData.PropertyChanged += BoundData_PropertyChanged;

    IsDirty = false;
}

void BoundData_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
    IsDirty = true;
}

// implementation of the IsDirty flag
public bool IsDirty
{
    get
    {
        return _isDirty;
    }
    set
    {
        _isDirty = value;
        NotifyOfPropertyChange("IsDirty");
    }
}

The problem is that the BoundData_PropertyChanged event handler is hit due to the form being initialized from the database AFTER the Initialize() method is finished. So, the IsDirty flag is set to true, and the Save button is enabled, even though the form has just loaded and the user has not changed anything. What am I missing? Surely this is a common problem but I have been unable to find a good solution. This is my first MVVM project so it is entirely possible I am missing some basic concept.

UPDATE: To clarify, I believe the issue is that I need to be able to hook into an event or callback that will fire when all of the bindings are done updating, so I can then wire up the PropertyChanged event handlers.

One thing that you can do that might help is to set up the property triggering the change like so:

    public virtual bool Prop1
    {
       get
       {
            return _prop1;
       }
       set
       {
            if (_prop1 != value)
            {
                _prop1 = value;
                NotifyOfPropertyChange("IsDirty");
            }
       }

That way, the event will only trigger if the value was actually changed, and not just set redundantly. This of course assumes that the value is not actually changing in your case.

I propose you to design a generic method call. Ie something like ValueChanged event handler (custom event handler), which in turn call the BoundData_PropertyChanged. This means, you can call this ValueChanged method/handler whenever there is a change in control.

Eg

    void ValueChanged(object sender, EventArgs e)
    {
    BoundData_PropertyChanged();
    }

   textBox.TextChanged += new System.EventHandler(ValueChanged);

Syntax may not be accurate but you can guess what I am proposing here.

This solution may not be infallible, but it is working for me in the limited test cases I have utilised.

On first entry the EntityKey value will be null. Check for this.

/// <summary>
/// Log the invoice status change
/// </summary>
/// <param name="value">The value that the invoice status is changing to</param>
partial void OnInvoiceStatusValueChanging(string value)
{
    var newStatus = ConvertInvoiceStatus(value);
    if (this.EntityKey != null && InvoiceStatus != newStatus)
    {
        AddNewNote(string.Format("Invoice status changing from [{0}] to [{1}]", InvoiceStatus.GetDescription(), newStatus.GetDescription()));
    }
}

/// <summary>
/// Log the invoice status change
/// </summary>
partial void OnInvoiceStatusValueChanged()
{
    if (this.EntityKey != null)
        AddNewNote(string.Format("Invoice status changed to [{0}]", InvoiceStatus.GetDescription()));
}

I know this question is ancient, but I had the exact same issue and had a very hard time figuring it out. I'm pretty new to WPF/MVVM and probably didn't know the right things to Google or ask. Here's my solution. Hopefully it helps somebody.

I have an IsDirty flag almost identical to the one in the original post. The only difference is that I added a command to reset it to false when the view finishes rendering. The basic idea came from Notify ViewModel when View is rendered/instantiated

Fire a Loaded event in the View:

<UserControl x:Class="MyUserControl"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity">
<i:Interaction.Triggers>
    <i:EventTrigger EventName="Loaded">
        <i:InvokeCommandAction Command="{Binding Path=OnLoadedCommand}"/>
    </i:EventTrigger>
</i:Interaction.Triggers>

Then in the view model set the IsDirty flag to false when the event fires.

public ICommand OnLoadedCommand { get; private set; }

// Constructor
public MyUserControlViewModel()
{
    OnLoadedCommand = new DelegateCommand(OnLoaded);
}

public void OnLoaded()
{
  // Ignore any PropertyChanged events that fire 
  // before the UserControl is rendered.  
   IsDirty = false; 
}

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