Silverlight MVVM binding updates fire in undesired order

≡放荡痞女 提交于 2019-12-05 09:17:34

You could add a behavior to your textbox to updated the binding every time the text is changed in the textbox. Maybe this solved your problems.

Here´s the code for the Behavior class:

    public class UpdateTextBindingOnPropertyChanged : Behavior<TextBox> {
    // Fields
    private BindingExpression expression;

    // Methods
    protected override void OnAttached() {
        base.OnAttached();
        this.expression = base.AssociatedObject.GetBindingExpression(TextBox.TextProperty);
        base.AssociatedObject.TextChanged+= OnTextChanged;
    }

    protected override void OnDetaching() {
        base.OnDetaching();
        base.AssociatedObject.TextChanged-= OnTextChanged;
        this.expression = null;
    }

    private void OnTextChanged(object sender, EventArgs args) {
        this.expression.UpdateSource();
    }
}

Heres the XAML:

xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" 
xmlns:local="Namespace of the class where UpdateTextBindingOnPropertyChanged is defined"

<TextBox Text="{Binding SelectedItem.Content, Mode=TwoWay}">
  <i:Interaction.Behaviors>
    <local:UpdateTextBindingOnPropertyChanged />
  </i:Interaction.Behaviors>
</TextBox >

This is one solution we currently came up with. It has the advantage that it separates different tasks to the appropriate layer. For example, the View enforces an update of the binding, while the ViewModel tells the View to do so. Another advantage is that its handled synchronously, which would for example allow to check the content right before switching away, and the call-stack remains unchanged without raising "External Code" (Going over Dispatcher or even DispatcherTimer would do so) which is better for maintenance and flow control. A disadvantage is the new Event which has to be bound and handled (and finally unbound. I present an anonymous handler only for example reasons).

How to get there?

In ViewModelBase, implement a new ForceBindingUpdate event:

public abstract class ViewModelBase : INotifyPropertyChanged
{
    // ----- leave everything from original code ------

    public event EventHandler ForceBindingUpdate;
    protected void OnForceBindingUpdate()
    {
        var handler = ForceBindingUpdate;
        if (handler != null)
            handler(this, EventArgs.Empty);
    }
}

In MainViewModel, update the setter of the SelectedItem property:

set // of SelectedItem Property
{
    if (_selectedViewModel != value)
    {
        // Ensure Data Update - the new part
        OnForceBindingUpdate();

        // Old stuff
        _selectedViewModel = value;
        OnPropertyChanged("SelectedItem");
    }
}

Update the MvvmTestView Code Behind to implement the new event:

void MvvmTestView_Loaded(object sender, RoutedEventArgs e)
{
    // remains unchanged
    Mvvm.MainViewModel viewModel = new Mvvm.MainViewModel();
    viewModel.Items.Add(new Mvvm.ItemViewModel("Hello StackOverflow"));
    viewModel.Items.Add(new Mvvm.ItemViewModel("Thanks to Community"));

    // Ensure Data Update by rebinding the content property - the new part
    viewModel.ForceBindingUpdate += (s, a) =>
    {
        var expr = ContentTextBox.GetBindingExpression(TextBox.TextProperty);
        expr.UpdateSource();
    };

    // remains unchanged
    DataContext = viewModel;
}

Last but not least, the minimal XAML Update: Give the TextBox a name by adding x:Name="ContentTextBox" Attribute to the TextBoxs XAML.

Done.

Actually, I don't know if this is the cleanest solution, but it gets close to what we had in mind.

Maybe you could handle TextBox LostFocus then (instead of listening to every key press)?

Other idea would be to keep a proxy property on the ViewModel instead of directly binding to SelectedItem.Content and writing some code to make sure the item is updated.

Solution №1

public class LazyTextBox: TextBox
{
    //bind to that property instead..
    public string LazyText
    {
        get { return (string)GetValue(LazyTextProperty); }
        set { SetValue(LazyTextProperty, value); }
    }

    public static readonly DependencyProperty LazyTextProperty =
        DependencyProperty.Register("LazyText", typeof(string), typeof(LazyTextBox), 
        new PropertyMetadata(null));

    //call this method when it's really nessasary...
    public void EnsureThatLazyTextEqualText()
    {
        if (this.Text != this.LazyText)
        {
            this.LazyText = this.Text;
        }
    }
}

Solution №2 (works as magic :) )

public class MainViewModel : ViewModelBase
{
    private ObservableCollection<ItemViewModel> _items = 
            new ObservableCollection<ItemViewModel>(); 
    private ItemViewModel _selectedViewModel; 
    public ObservableCollection<ItemViewModel> Items { get { return _items; } } 
    public ItemViewModel SelectedItem 
    { 
        get { return _selectedViewModel; }
        set
        {
            if (_selectedViewModel != value)
            {
                if (SelectedItem != null)
                {
                    SelectedItem.Content = SelectedItem.Content;
                }

                _selectedViewModel = value;

                // A little delay make no harm :)
                var t = new DispatcherTimer();
                t.Interval = TimeSpan.FromSeconds(0.1);
                t.Tick += new EventHandler(t_Tick);
                t.Start();
            }
        } 
    }

    void t_Tick(object sender, EventArgs e)
    {
        OnPropertyChanged("SelectedItem");
        (sender as DispatcherTimer).Stop();
    }
}
mpaulson

I know that in MVVM we do not want to put code in code behind. But in this instance it hurts nothing as it is entirely maintained in the UI and SOP is maintained.

By putting a ghost element to take focus we can swap the focus back in forth forcing the text box to commit its contents. So in the code behind we take care of the focus wiggle.

But yet we still are using a relay command Update Command to execute the save. So the order is good as the Click event fires wiggling the view. And then the relay command UpdateCommand will fire and the textbox is committed and ready for update.

<MenuItem Header="_Save" 
   Command="{Binding UpdateCommand}" Click="MenuItem_Click">
</MenuItem>
private void MenuItem_Click(object sender, RoutedEventArgs e)
{
    UIElement elem = Keyboard.FocusedElement as UIElement;
    Keyboard.Focus(ghost);
    Keyboard.Focus(elem);
}

Solution #3

public abstract class ViewModelBase : INotifyPropertyChanged 
{
    private List<string> _propNameList = new List<string>();

    public event PropertyChangedEventHandler PropertyChanged; 
    protected void OnPropertyChanged(string propertyName) 
    { 
        var handler = PropertyChanged;
        if (handler != null)
            _propNameList.Add(propertyName);

        var t = new DispatcherTimer();  
        t.Interval = TimeSpan.FromSeconds(0);
        t.Tick += new EventHandler(t_Tick);             
        t.Start();
    }

    void t_Tick(object sender, EventArgs e)
    {
        if (_propNameList.Count > 0)
        {
            var handler = PropertyChanged;
            if (handler != null)
                handler(this, new PropertyChangedEventArgs(_propNameList[0]));

            _propNameList.Remove(_propNameList[0]);
        }
    } 
}

PS: it's the same timer.. but this solution is more generic..

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!