Обновления привязки Silverlight MVVM запускаются в нежелательном порядке - PullRequest
5 голосов
/ 17 марта 2011

Сценарий : В проекте Silverlight 4 MVVM у нас есть элемент управления ListBox, содержащий элементы, выбранный элемент имеет двустороннюю привязку к соответствующему свойству в ViewModel. Другой элемент управления (например, я сократил его до одного TextBox) - это данные, привязанные к содержимому выбранного элемента. Значение должно обновляться при отпуске / потеря фокуса.

Проблема : Когда значение в TextBox изменяется, и мы оставляем это TextBox нажатием клавиши Tab, все работает как нужно - значение обновляется. Однако, если пользователь щелкает по другому элементу в ListBox, установщик SelectedItem запускается до того, как запускается содержимое установщика TextBox, не оставляя шансов обработать пользовательский ввод.

Screen

В отладчике при добавлении точек останова к установщикам свойств можно видеть, что новый выбор ListView применяется в первую очередь, до обработки обновления TextBox.

Желаемое поведение : Нам нужно знать, что текущий выбранный элемент был изменен до того, как пользователь выбрал другой элемент. Не желательно иметь пользовательский триггер обновления, который бы уведомлял о каждом нажатии клавиши (мы знаем, что это возможно).

Можете ли вы помочь?

Код (очень простой пример):

ViewModel

public abstract class ViewModelBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged(string propertyName)
    {
        var handler = PropertyChanged;
        if (handler != null)
            handler(this, new PropertyChangedEventArgs(propertyName));
    }
}

public class ItemViewModel : ViewModelBase
{
    private string _content;

    public ItemViewModel(string initContent)
    {
        _content = initContent;
    }

    public string Content
    {
        get
        {
            return _content;
        }
        set
        {
            if (_content != value)
            {
                _content = value;
                OnPropertyChanged("Content");
            }
        }
    }
}

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)
            {
                _selectedViewModel = value;
                OnPropertyChanged("SelectedItem");
            }
        }
    }
}

XAML

<Grid x:Name="LayoutRoot" Background="White">
    <ListBox Height="100"
             HorizontalAlignment="Left"
             Margin="12,12,0,0"
             VerticalAlignment="Top"
             ItemsSource="{Binding Items}"
             SelectedItem="{Binding SelectedItem, Mode=TwoWay}"
             DisplayMemberPath="Content"
             Width="220" />
    <TextBox Height="23"
             HorizontalAlignment="Left"
             Margin="12,118,0,0"
             Text="{Binding SelectedItem.Content, Mode=TwoWay}"
             VerticalAlignment="Top"
             Width="220" />
</Grid>

Код XAML позади

    public MvvmTestView()
    {
        InitializeComponent();

        Loaded += new RoutedEventHandler(MvvmTestView_Loaded);
    }

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

        DataContext = viewModel;
    }

ОБНОВЛЕНИЕ 1 Я представляю самостоятельно разработанное решение для вас, чтобы проверить, которое, вероятно, будет принятым, я все же хочу призвать вас комментировать и давать подсказки. Спасибо.

Ответы [ 7 ]

3 голосов
/ 17 марта 2011

Вы можете добавить поведение в текстовое поле, чтобы обновлять привязку каждый раз, когда текст изменяется в текстовом поле.Возможно, это решило ваши проблемы.

Вот код для класса поведения:

    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();
    }
}

Вот 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 >
1 голос
/ 22 марта 2011

Это одно решение, которое мы в настоящее время придумали. Преимущество состоит в том, что он разделяет различные задачи на соответствующий уровень. Например, представление принудительно обновляет привязку, а ViewModel указывает представлению сделать это. Другое преимущество заключается в том, что он обрабатывается синхронно, что, например, позволяет проверять содержимое непосредственно перед переключением, а стек вызовов остается неизменным без повышения значения «Внешний код» (при переходе на Dispatcher или даже DispatcherTimer) что лучше для обслуживания и контроля потока. Недостатком является новое Событие, которое должно быть связано и обработано (и, наконец, не связано. Я представляю анонимный обработчик только для примера).

Как туда добраться?

В ViewModelBase реализовать новое событие ForceBindingUpdate:

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);
    }
}

В MainViewModel обновите установщик свойства SelectedItem:

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

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

Обновите код MvvmTestView Behind для реализации нового события:

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;
}

Последнее, но не менее важное, минимальное обновление XAML: присвойте TextBox имя, добавив x:Name="ContentTextBox" атрибут к TextBox s XAML.

Готово.

На самом деле, я не знаю, является ли это самым чистым решением, но оно приближается к тому, что мы имели в виду.

0 голосов
/ 24 января 2012

Я знаю, что в MVVM мы не хотим помещать код в код позади. Но в этом случае это ничего не ранит, поскольку полностью поддерживается в пользовательском интерфейсе, а SOP поддерживается.

Поместив элемент-призрак в фокус, мы можем переключить фокус обратно на следующий текстовое поле для фиксации его содержимого. Так что в коде позади мы позаботимся о покачивании фокуса.

Но мы все еще используем команду обновления Команда обновления для выполнения сохранения. Так что порядок хорош, так как событие Click запускает вид. И тогда сработает команда ретрансляции UpdateCommand, и текстовое поле будет зафиксировано и готово к обновлению.

<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);
}
0 голосов
/ 18 марта 2011

Решение № 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: это тот же таймер .. но это решение более общее ..

0 голосов
/ 17 марта 2011

Раствор №2 (работает как по волшебству :))

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();
    }
}
0 голосов
/ 17 марта 2011

Раствор №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;
        }
    }
}
0 голосов
/ 17 марта 2011

Может быть, тогда вы могли бы обработать TextBox LostFocus (вместо прослушивания каждого нажатия клавиши)?

Другая идея состояла бы в том, чтобы сохранить свойство прокси в ViewModel вместо прямой привязки к SelectedItem.Content и написания некоторого кода для обеспечения обновления элемента.

...