绑定到对象的 ListView 中的文本框,两种方式绑定不起作用
TextBox inside a ListView bound to an object, two way binding dosen't work
编辑:
好吧,经过无数次无果而终的尝试,我创建了一个非常小的 Wpf 应用程序。您可以直接复制此代码。请注意,当您更改 TextBox 中的值并按下 Test 按钮时,这些值永远不会更新。我不明白为什么这两种方式绑定不起作用。请帮忙
这是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));
}
}
But when I change the values in the TextBox it dosen't update back the source that is the CurrentParameterValue property.
Binding
in ListView
不知道如何更新 object
类型的 属性 因为它是 ItemsSource
并且它只能更新 ICollection
例如你不能像 C# 中的 List
那样与 object
交互。例如:
object MyList = new object();
MyList.Add("something"); // Compile error
And in my viewmodel the object which can be a list of long, list of double etc comes from an external 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
是默认模式,您可以不在此处明确包含它。
UpdateSourceTrigger=PropertyChanged
(默认为LostFocus
)在UI->VM方向需要,而不是在后面。所以,在这里没用。您可以将其应用于模板中的 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。
您的绑定问题是您试图绑定到一个对象。这在 OneWay
/OneTime
场景中完全没问题。但在使用绑定 TwoWay
时不是。您可以更改 属性 的值,例如在您的视图模型中,但您不能更改对象实例本身。在您的特定情况下,绑定必须将新的 long
输入发送到视图模型的值集合并替换旧值。当然,这永远不会发生,因为 Binding
并非设计为以这种方式工作。
技术原因是更改实例将意味着更改 Binding.Source
。一旦绑定处于活动状态(由 BindingExpression
控制),它就变得不可变。不允许更改源。这也是 {Binding Source={DynamicResource ...}}
不起作用的原因。 BindingSource
只能是静态的(或 StaticResource
- 不改变资源)。
您通常绑定到属性。在 TwoWay
绑定场景中 Binding
可以简单地更新 属性 的值。因此,您的问题的解决方案是将 long
值包装到 class 并将 TextBox
绑定到此 class 到 [=119= 的 属性 ] 实际值。
在这种情况下,您的代码看起来太复杂了。
您的对象结构太复杂或不自然。
您不需要将 DataTemplate
应用于 ContentControl
(在 XAML 中)。
当然,由于这是一个 UWP 应用程序,请尽可能使用 x:Bind
,因为它会提高性能。转换器是冗余的,因为 Binding
和 x:Bind
允许嵌套 PropertyPath
例如
<ListView ItemsSource="{Binding CurrentParameterValue.ListParameterValues}">
ItemsControl.ItemsSource
不需要 TwoWay
绑定。 ItemsControl
永远不会 update/replace 源集合。如果您不打算替换视图模型中的源集合(例如 AtlasMethodParameterList = new ObservableCollection<>()
),那么您甚至可以将绑定模式设置为 OneTime
(这将是 x:Bind
的默认设置) .
我建议使用 OneTime
,如果您需要替换集合,则在集合上调用 Clear()
并添加新项目。这将提高性能。
从不 在方法签名中使用 async void
,事件处理程序除外。
当 return 类型为 void
或 return 值 async Task<TResult>
时,始终使用 async Task
。否则你会遇到意想不到的副作用,尤其是遇到异常的时候:
// An async void method must return Task
private async Task GetParameterList(string obj)
另外 async
方法应该总是 等待。这意味着调用和等待 async
方法的方法本身必须 return Task
或 Task<T>
是可等待的。无法等待方法 returning 类型 void
。
每个控件的所有 DependencyProperty
,默认情况下将它们的 Binding.UpdateSourceTrigger
设置为 UpdateSourceTrigger.PropertyChanged
。
例外是可能引发过多连续 属性 更改的属性,例如 TextBox
会在每次 input/key 按下时执行。 TextBox.Text
默认设置为 UpdateSourceTrigger.LostFocus
。
您应该从绑定中删除所有冗余 UpdateSourceTrigger.PropertyChanged
以提高可读性。
如果您不打算读取变量,请考虑使用 out
而不是 ref
。如果您只设置值,则更喜欢使用 out
来暗示您对任何 reader 的意图。如果不想修改引用(只读引用),请使用 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>
编辑:
好吧,经过无数次无果而终的尝试,我创建了一个非常小的 Wpf 应用程序。您可以直接复制此代码。请注意,当您更改 TextBox 中的值并按下 Test 按钮时,这些值永远不会更新。我不明白为什么这两种方式绑定不起作用。请帮忙
这是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));
}
}
But when I change the values in the TextBox it dosen't update back the source that is the CurrentParameterValue property.
Binding
in ListView
不知道如何更新 object
类型的 属性 因为它是 ItemsSource
并且它只能更新 ICollection
例如你不能像 C# 中的 List
那样与 object
交互。例如:
object MyList = new object();
MyList.Add("something"); // Compile error
And in my viewmodel the object which can be a list of long, list of double etc comes from an external 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
是默认模式,您可以不在此处明确包含它。UpdateSourceTrigger=PropertyChanged
(默认为LostFocus
)在UI->VM方向需要,而不是在后面。所以,在这里没用。您可以将其应用于模板中的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。
您的绑定问题是您试图绑定到一个对象。这在 OneWay
/OneTime
场景中完全没问题。但在使用绑定 TwoWay
时不是。您可以更改 属性 的值,例如在您的视图模型中,但您不能更改对象实例本身。在您的特定情况下,绑定必须将新的 long
输入发送到视图模型的值集合并替换旧值。当然,这永远不会发生,因为 Binding
并非设计为以这种方式工作。
技术原因是更改实例将意味着更改 Binding.Source
。一旦绑定处于活动状态(由 BindingExpression
控制),它就变得不可变。不允许更改源。这也是 {Binding Source={DynamicResource ...}}
不起作用的原因。 BindingSource
只能是静态的(或 StaticResource
- 不改变资源)。
您通常绑定到属性。在 TwoWay
绑定场景中 Binding
可以简单地更新 属性 的值。因此,您的问题的解决方案是将 long
值包装到 class 并将 TextBox
绑定到此 class 到 [=119= 的 属性 ] 实际值。
在这种情况下,您的代码看起来太复杂了。
您的对象结构太复杂或不自然。
您不需要将 DataTemplate
应用于 ContentControl
(在 XAML 中)。
当然,由于这是一个 UWP 应用程序,请尽可能使用 x:Bind
,因为它会提高性能。转换器是冗余的,因为 Binding
和 x:Bind
允许嵌套 PropertyPath
例如
<ListView ItemsSource="{Binding CurrentParameterValue.ListParameterValues}">
ItemsControl.ItemsSource
不需要 TwoWay
绑定。 ItemsControl
永远不会 update/replace 源集合。如果您不打算替换视图模型中的源集合(例如 AtlasMethodParameterList = new ObservableCollection<>()
),那么您甚至可以将绑定模式设置为 OneTime
(这将是 x:Bind
的默认设置) .
我建议使用 OneTime
,如果您需要替换集合,则在集合上调用 Clear()
并添加新项目。这将提高性能。
从不 在方法签名中使用 async void
,事件处理程序除外。
当 return 类型为 void
或 return 值 async Task<TResult>
时,始终使用 async Task
。否则你会遇到意想不到的副作用,尤其是遇到异常的时候:
// An async void method must return Task
private async Task GetParameterList(string obj)
另外 async
方法应该总是 等待。这意味着调用和等待 async
方法的方法本身必须 return Task
或 Task<T>
是可等待的。无法等待方法 returning 类型 void
。
每个控件的所有 DependencyProperty
,默认情况下将它们的 Binding.UpdateSourceTrigger
设置为 UpdateSourceTrigger.PropertyChanged
。
例外是可能引发过多连续 属性 更改的属性,例如 TextBox
会在每次 input/key 按下时执行。 TextBox.Text
默认设置为 UpdateSourceTrigger.LostFocus
。
您应该从绑定中删除所有冗余 UpdateSourceTrigger.PropertyChanged
以提高可读性。
如果您不打算读取变量,请考虑使用 out
而不是 ref
。如果您只设置值,则更喜欢使用 out
来暗示您对任何 reader 的意图。如果不想修改引用(只读引用),请使用 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>