【问题标题】:TextBox inside a ListView bound to an object, two way binding dosen't workListView中的TextBox绑定到一个对象,两种方式绑定不起作用
【发布时间】:2020-05-23 00:09:04
【问题描述】:

编辑:

好吧,终于在没有运气的情况下玩了无数次,我创建了一个非常小的 Wpf 应用程序。您可以直接复制此代码。请注意,当您更改 TextBox 中的值并按下“测试”按钮时,这些值永远不会更新。我不明白为什么两种方式绑定不起作用。请帮忙。

这里是 xaml:

<Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <ListView Grid.Row="0" 
                 ItemsSource="{Binding Path=Demo.CurrentParameterValue,Mode=TwoWay}" 
                 HorizontalAlignment="Center" VerticalAlignment="Center">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <TextBox Text="{Binding Path=.,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" Width="100"></TextBox>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>

        <Button Grid.Row="1" Click="Button_Click">TEST</Button>
    </Grid>

这里是 xaml.cs:

namespace WpfApp9
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        private VmServiceMethodsViewDataGridModel _demo;

        public event PropertyChangedEventHandler PropertyChanged;
        protected void OnPropertyChanged(string name = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
        }
        public VmServiceMethodsViewDataGridModel Demo
        {
            get => _demo;
            set
            {
                _demo = value;
                OnPropertyChanged("Demo");
            }
        }

        public MainWindow()
        {
            InitializeComponent();
            DataContext = this;
            Demo = new VmServiceMethodsViewDataGridModel();
            Demo.CurrentParameterValue.Add(1);
            Demo.CurrentParameterValue.Add(2);
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            var collection = Demo.CurrentParameterValue;
            MessageBox.Show(string.Format("Values are {0}, {1}", collection[0], collection[1]));
        }
    }

    public class VmServiceMethodsViewDataGridModel : INotifyPropertyChanged
    {
        private List<object> _currentParameterValue;
        public List<object> CurrentParameterValue
        {
            get => _currentParameterValue;
            set
            {
                _currentParameterValue = value;
                OnPropertyChanged("CurrentParameterValue");
            }
        }

        public VmServiceMethodsViewDataGridModel()
        {
            CurrentParameterValue = new List<object>();
        }

        public event PropertyChangedEventHandler PropertyChanged;
        protected void OnPropertyChanged(string name = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
        }
    }

【问题讨论】:

    标签: c# wpf xaml datatemplate


    【解决方案1】:

    您的绑定问题在于您试图绑定到一个对象。这在OneWay/OneTime 场景中非常好。但在使用绑定TwoWay 时不会。您可以更改属性的值,例如在您的视图模型中,但您不能更改对象实例本身。在您的特定情况下,绑定必须将新的 long 输入发送到视图模型的值集合并替换旧值。当然,这永远不会发生,因为 Binding 并非旨在以这种方式工作。
    技术原因是更改实例意味着更改Binding.Source。一旦绑定处于活动状态(由BindingExpression 控制),它就会变得不可变。不允许更改源。这也是{Binding Source={DynamicResource ...}} 不起作用的原因。 BindingSource 只能是静态的(或 StaticResource - 不改变资源)。

    您通常绑定到属性。在TwoWay 绑定场景中,Binding 可以简单地更新属性的值。因此,解决您的问题的方法是将long 值包装到一个类中,并将TextBox 绑定到该类的属性以检索/修改实际值。

    在这种情况下,您的代码看起来太复杂了。
    您的对象结构过于复杂或不自然。

    您无需将 DataTemplate 应用于 ContentControl(在 XAML 中)。
    当然,由于这是一个 UWP 应用程序,请尽可能使用x:Bind,因为它会提高性能。转换器是冗余的,因为Bindingx:Bind 允许嵌套PropertyPath,例如

    <ListView ItemsSource="{Binding CurrentParameterValue.ListParameterValues}">
    

    ItemsControl.ItemsSource 不需要 TwoWay 绑定。 ItemsControl 永远不会更新/替换源集合。如果您不打算替换视图模型中的源集合(例如,AtlasMethodParameterList = new ObservableCollection&lt;&gt;()),那么您甚至可以将绑定模式设置为 OneTime(这将是 x:Bind 的默认值)。
    我建议使用OneTime,如果您需要替换集合,请在集合上调用Clear() 并添加新项目。这将提高性能。

    从不在方法签名中使用async void,事件处理程序除外。
    当返回类型为void 或返回值async Task&lt;TResult&gt; 时,始终使用async Task。否则你会遇到意想不到的副作用,尤其是遇到异常时:

    // An async void method must return Task
    private async Task GetParameterList(string obj)
    

    另外,async 方法应该始终等待。这意味着调用和等待async 方法的方法本身必须返回TaskTask&lt;T&gt; 才能等待。无法等待返回类型 void 的方法。

    每个控件的所有DependencyProperty,默认情况下将它们的Binding.UpdateSourceTrigger设置为UpdateSourceTrigger.PropertyChanged
    例外情况是可能会引发太多连续属性更改的属性,例如 TextBox 在每次输入/按键时都会执行的操作。 TextBox.Text 的默认设置为 UpdateSourceTrigger.LostFocus
    您应该从绑定中删除所有多余的 UpdateSourceTrigger.PropertyChanged 以提高可读性。

    如果您不打算读取变量,请考虑使用out 而不是ref。如果您只设置值,则更喜欢使用out 向任何读者暗示您的意图。如果不打算修改引用(只读引用),请使用 in
    您的 Set 方法应该如下所示:

    protected virtual void Set<TValue>(out TValue valueTarget, TValue value, [CallerMemberName] string propertyName = null)
    {
      if (value != valueTarget)
      {
        valueTarget = value;
        OnPropertyChanged(propertyName);
      }
    }
    

    我重构了你的完整代码,试图改进它:

    Parameter.cs

    // The type that wraps the actual parameter value.
    // Consider to use dedicated types e.g., LongParameter instead, to allow a strongly typed Value property instead of a basic property of type object.
    // This prevents implicit boxing/unboxing in order to convert from object/reference type to primitive/value type and vice versa. This will improve performance. 
    // (Only needed because we are dealing with primitive/value types like long, double, etc)
    // You would then have to define a DataTemplate for each type. Don't forget to set x:DataType on each DataTemplate.
    public class Parameter : BindableBase
    {
      protected Parameter(object value)
      {
        this.Value = value;
      }
    
      private object value;
      public object Value
      {
        get => this.value;
        set => Set(out this.value, value);
      }
    }
    

    VmServiceModel.cs

    public class VmServiceModel : BindableBase
    {    
      public VmServiceModel()
      {
        this.Parameters = new List<Parameter>();
      }
    
      private List<Parameter> _parameters;
      public List<Parameter> Parameters
      {
        get => this._parameters;
        set => Set(out this._parameters, value);
      }
    }
    

    ViewModel.cs

    public class ViewModel : INotifyPropertyChanged
    {
      public ViewModel()
      {
        this.AtlasMethodParameterList = new ObservableCollection<VmServiceModel>();
      }
    
      private ObservableCollection<VmServiceModel> _atlasMethodParameterList;
      public ObservableCollection<VmServiceModel> AtlasMethodParameterList
      {
        get => _atlasMethodParameterList;
        set => Set(out _atlasMethodParameterList, value);
      }
    
      private async Task GetParameterList(string obj)
      {
        foreach (var item in this.ParametersCollection)
        {
          var vmServiceModel = new VmServiceModel();
          vmServiceModel.Parameters
            .AddRange(item.Value.Cast<long>().Select(innerItem => new Parameter(innerItem)));
    
          this.AtlasMethodParameterList.Add(vmServiceModel);
        }
      }
    }
    

    MainPage.xaml.cs

    public sealed partial class MainPage : Page
    {
      public ViewModel ViewModel { get; set; }
    
      public MainPage()
      {
        this.InitializeComponent();
        this.ViewModel = new ViewModel();
      }
    }
    

    MainPage.xaml

    <Page>
      <Page.Resources>
        <DataTemplate x:Key="ListIntTemplate" x:DataType="local:VmServiceModel">
          <ListView ItemsSource="{x:Bind Parameters}"
                    HorizontalAlignment="Center" 
                    SelectionMode="None" Background="Transparent">
            <ListView.ItemsPanel>
              <ItemsPanelTemplate>
                <controls:WrapPanel VerticalAlignment="Top"/>
              </ItemsPanelTemplate>
            </ListView.ItemsPanel>
            <ListView.ItemTemplate>
              <DataTemplate x:DataType="local:Parameter">
                <TextBox Text="{Binding Value Mode=TwoWay}" Height="36" Width="65"/>
              </DataTemplate>
            </ListView.ItemTemplate>
          </ListView>
        </DataTemplate>
      </Page.Resources>
    
      <Grid>
        <ListView ItemsSource="{x:Bind ViewModel.AtlasMethodParameterList}" 
                  ItemTemplate="{StaticResource ListIntTemplate}">
        </ListView>
      </Grid>
    </Page>
    

    【讨论】:

    • 非常感谢@BionicCode。我将遵循这种方法。那是 async void 因为它是一个事件处理程序。当我复制代码的点点滴滴时,我没有更新它。
    • 很高兴能为您提供帮助。我觉得我应该提到所有可以改进的地方。我希望你没有弄错。让我知道是否可以帮助您。
    • 非常感谢@BionicCode,您的帖子对上述答案有很大帮助。我已经修改了我的代码,它现在可以工作了。
    • 对不起@bionicCode 昨天,我没有正确测试。一个案例正在工作,我认为一切正常。所以我最终创建了一个新应用程序。请查看我更新的代码。我很抱歉来回走动。
    • 谢谢@BionicCode 我完全按照你建议的方式做了,而且效果很好。现在我完美地测试了它。非常感谢:)
    【解决方案2】:

    但是当我更改 TextBox 中的值时,它不会更新回作为 CurrentParameterValue 属性的源。

    ListView 中的Binding 不知道如何更新object 类型的属性,因为它是ItemsSource,它只能更新ICollection,比如你不能像@ 一样与object 交互C# 中的 987654330@。例如:

    object MyList = new object();
    MyList.Add("something"); // Compile error
    

    在我的视图模型中,可以是长列表、双列表等的对象来自外部 API。

    那么你需要这个解决方案。

    public class VmServiceMethodsViewDataGridModel : BindableBaseThreadSafe
    {
        private List<object> _currentParameterValue; // or ObservableCollection
        public List<object> CurrentParameterValue
        {
            get => _currentParameterValue;
            set => Set(ref _currentParameterValue, value);
        }
    }
    

    另外

    我不知道你想用这种语法实现或解决什么

    <ListView ItemsSource="{x:Bind ViewModel.AtlasMethodParameterList,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}">
    

    一切都必须使用这个

    <ListView ItemsSource="{Binding AtlasMethodParameterList}">
    
    • Mode=TwoWay 是默认模式,您可以不在这里明确包含它。
    • UI->VM 方向需要UpdateSourceTrigger=PropertyChanged(默认为LostFocus),而不是反向。所以,这里没用。您可以将其应用于模板中的TextBox

    编辑

    因为双向Binding 需要显式Path 并且目标必须是包含Setter 的属性。

    演示应用的解决方法

    <ListView Grid.Row="0" 
              ItemsSource="{Binding Demo.CurrentParameterValue}" 
              HorizontalAlignment="Center" VerticalAlignment="Center">
        <ListView.ItemTemplate>
            <DataTemplate>
                <TextBox Text="{Binding Value, UpdateSourceTrigger=PropertyChanged}" Width="100"></TextBox>
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>
    
    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        private VmServiceMethodsViewDataGridModel _demo;
    
        public event PropertyChangedEventHandler PropertyChanged;
        protected void OnPropertyChanged(string name = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
        }
        public VmServiceMethodsViewDataGridModel Demo
        {
            get => _demo;
            set
            {
                _demo = value;
                OnPropertyChanged("Demo");
            }
        }
    
        public MainWindow()
        {
            InitializeComponent();
            DataContext = this;
            Demo = new VmServiceMethodsViewDataGridModel();
            Demo.CurrentParameterValue.Add(new MyItem { Value = 1 });
            Demo.CurrentParameterValue.Add(new MyItem { Value = 2 });
        }
    
        private void Button_Click(object sender, RoutedEventArgs e)
        {
            var collection = Demo.CurrentParameterValue;
            MessageBox.Show(string.Format("Values are {0}, {1}", collection[0].Value, collection[1].Value));
        }
    }
    
    // here it is
    public class MyItem
    {
        public object Value { get; set; }
    }
    
    public class VmServiceMethodsViewDataGridModel : INotifyPropertyChanged
    {
        private List<MyItem> _currentParameterValue;
        public List<MyItem> CurrentParameterValue
        {
            get => _currentParameterValue;
            set
            {
                _currentParameterValue = value;
                OnPropertyChanged("CurrentParameterValue");
            }
        }
    
        public VmServiceMethodsViewDataGridModel()
        {
            CurrentParameterValue = new List<MyItem>();
        }
    
        public event PropertyChangedEventHandler PropertyChanged;
        protected void OnPropertyChanged(string name = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
        }
    }
    

    此外,您可以根据自己的需要为Value 实施INPC。

    【讨论】:

    猜你喜欢
    • 2012-01-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-08-15
    • 2020-04-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多