【问题标题】:Silverlight MVVM binding updates fire in undesired orderSilverlight MVVM 绑定更新以不希望的顺序触发
【发布时间】:2011-07-17 08:23:12
【问题描述】:

场景:在 Silverlight 4 MVVM 项目中,我们有一个包含项目的 ListBox 控件,所选项目被双向绑定到 ViewModel 中的适当属性。另一个控件(例如原因,我已将其简化为单个 TextBox)是绑定到所选项目内容的数据。该值应在离开/失去焦点时更新。

问题:当TextBox 中的值发生更改并且我们通过按Tab 键离开TextBox 时,一切都按预期工作- 值已更新。但是,如果用户单击 ListBox 中的其他项目,则 SelectedItem 设置器会在TextBox 设置器的内容被触发之前触发,从而没有机会处理用户输入。

您可以在调试器中看到,当向属性设置器添加断点时,会首先应用新的 ListView 选择,然后再处理 TextBox 更新。

期望行为:我们需要知道在用户选择另一个项目之前,当前选择的项目已被修改。不希望有一个自定义更新触发器,它会在每次按键时通知(我们知道这是可能的)。

你能帮忙吗?

代码(一个非常简单的例子):

视图模型

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 我提供a self designed solution 供您查看,这可能是被接受的,我仍然想鼓励您制作 cmets 并给出您的提示。谢谢。

【问题讨论】:

    标签: silverlight data-binding silverlight-4.0 mvvm


    【解决方案1】:

    您可以在文本框中添加一个行为,以在每次文本框中的文本发生更改时更新绑定。也许这解决了你的问题。

    这是 Behavior 类的代码:

        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 >
    

    【讨论】:

    • 虽然我喜欢这个解决方案(它正在工作!),但正如我所提到的,“不希望有一个自定义更新触发器,它会在每次按键时通知(我们知道这是可能的)。”我们已经为有意义的内联搜索框提供了这样的解决方案。在这种情况下,我们希望保留焦点丢失事件。我尝试更新您的代码以使用“LostFocus”而不是“TextChanged”,但不幸的是没有奏效。
    【解决方案2】:

    这是我们目前提出的一种解决方案。它的优点是将不同的任务分离到适当的层。例如,View 强制更新绑定,而 ViewModel 告诉 View 这样做。另一个优点是它同步处理,例如允许在切换之前检查内容,并且调用堆栈保持不变而不引发“外部代码”(通过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属性的setter:

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

    更新MvvmTestView Code 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 更新:通过将 x:Name="ContentTextBox" 属性添加到 TextBoxs XAML 来为 TextBox 命名。

    完成。

    实际上,我不知道这是否是最干净的解决方案,但它接近我们的想法。

    【讨论】:

      【解决方案3】:

      也许你可以处理 TextBox LostFocus (而不是听每一次按键)?

      其他想法是在 ViewModel 上保留一个代理属性,而不是直接绑定到 SelectedItem.Content 并编写一些代码来确保项目已更新。

      【讨论】:

      • 你好。处理 LostFocus 是不可接受的,我们有很多原因(特别是大规模的 MVVM 违规,它扼杀了 DataBinding 的优势)。
      • 关于“代理属性”,我看不出这将如何解决,而不仅仅是“转移”这个问题。谢谢
      • 好吧,您可以扩展 TextBox 并在更好的自定义控件中执行此操作 :) 本质上,您面临的是内置事件处理的问题,所以即使它不是最好的您可能必须找到一种方法来覆盖它。
      • 使用代理属性,您可以在设置器中检查之前的值是否已设置,然后再更改为新值(结合属性来存储之前选择的项目)。
      【解决方案4】:

      解决方案 №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;
              }
          }
      }
      

      【讨论】:

      • 虽然这看起来是一种合理的方法,但它留下了一些悬而未决的问题,主要问题是:应该触发“EnsureThatLazyTextEqualText()”以及何时触发? “当它真的需要时” - 嗯,在更改列表选择之前有必要,所以现在事情又变得复杂了......
      【解决方案5】:

      解决方案№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();
          }
      }
      

      【讨论】:

      • 你在开玩笑吗? ;-) 我的意思是,即使我真的很感谢您的努力并愿意提供帮助,但我们正在编写一个严肃的业务应用程序并寻找一个复杂解决方案 - 忘了提及(我在你的笑脸上看到了你放在答案和代码中,您对此并不认真-我希望至少如此;)。
      • 嗯..这段代码 100% 可行.. 这不是开玩笑......如果你不相信我,请使用这个t.Interval = TimeSpan.FromSeconds(0.000000000000000001);
      • 我完全理解这个问题。我知道绑定触发器是按特定顺序触发的。这个解决方案所做的是从不同的线程调用异步(!)触发方法,允许“跳过”(或退出)这个序列。虽然我接受这是 100% 的工作,但它根本不符合任何质量代码要求,这是一种解决方法,而且非常肮脏。
      【解决方案6】:

      我知道在 MVVM 中我们不想将代码放在代码后面。但在这种情况下,它并没有什么坏处,因为它完全在 UI 中维护并且 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);
      }
      

      【讨论】:

        【解决方案7】:

        解决方案 #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:这是同一个计时器..但是这个解决方案更通用..

        【讨论】:

        • 好的,我明白了这背后的主要思想。但是这种实现几乎是危险的。我猜想 Timer t 将永久提升 Tick,更糟糕的是,每次调用 OnPropertyChanged 时都会构造一个新的 Timer,而在事件处理程序仍然存在时,“旧” Timer 永远不会被破坏。这是一个定时炸弹(您需要在构造函数中实例化您的 Timer),但正如我所说,我明白这背后的想法,谢谢
        • TextBox 的问题.. 这是一个非常有名的包,从 Silverligth 2 就知道了.. TextBox 只是没有时间触发“绑定触发器”.. 所以我们现在唯一的方法就是给 TextBox 机会来触发这个触发器..
        猜你喜欢
        • 2013-02-02
        • 2011-02-28
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-07-02
        • 1970-01-01
        相关资源
        最近更新 更多