2

Edit:

Ok after finally playing around numerous times without no luck, I have created a very small Wpf application. You can directly copy this code. Notice when you change values in the TextBox and press the Test button, the values never get updated. I don't understand why the two way binding dosen't work. Please help.

Here is the 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>

Here is the 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));
        }
    }
nikhil
  • 1,578
  • 3
  • 23
  • 52

2 Answers2

2

The problem with your binding is that you are trying to bind to an object. This is perfectly fine in a OneWay/OneTime scenario. But not when using binding TwoWay. You can change the value of a property e.g. in your view model, but you can't change the object instance itself. In your specific case, the binding would have to send the new long input to the view model's value collection and replace the old value. Of course this will never happen as Binding is not designed to work this way.
The technical reason is that changing the instance would mean to change the Binding.Source. Once the binding is active (controlled by a BindingExpression) it becomes immutable. Changing the source is not allowed. That's also the reason why {Binding Source={DynamicResource ...}} won't work. The BindingSource can only be static (or StaticResource - not changing resource).

You usually bind to properties. In a TwoWay binding scenario Binding can simply update the property's value. So the solution to your problem is to wrap the long values into a class and bind the TextBox to a property of this class to retrieve/modify the actual value.

In this context your code looks too complicated.
Your object structure is too complex or unnatural.

You don't need to apply the DataTemplate to a ContentControl (in XAML).
And of course as this is a UWP application, use x:Bind where possible as it will improve performance. The converter is redundant as Binding and x:Bind allow a nested PropertyPath e.g.

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

ItemsControl.ItemsSource doesn't need a TwoWay binding. The ItemsControl will never update/replace the source collection. If you don plan to replace the source collection in the view model (e.g., AtlasMethodParameterList = new ObservableCollection<>()), then you can even set the binding mode to OneTime (which would be the default for x:Bind).
I recommend to use OneTime and if you need to replace the collection, rather call Clear() on the collection and add the new items. This will improve the performance.

Never use async void in a method signature except for event handlers.
Always use async Task, when the return type is void or when returning a value async Task<TResult>. Otherwise you will experience unexpected side effects, especially when encountering exceptions:

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

Also async methods should always be awaited. This means the method calling and awaiting an async method must itself return Task or Task<T> to be awaitable. A method returning type void cannot be awaited.

All DependencyProperty of every control, have their Binding.UpdateSourceTrigger set to UpdateSourceTrigger.PropertyChanged by default.
Exceptions are properties that are likely to raise too much consecutive property changes like a TextBox would do on each input/key press. TextBox.Text has the default set to UpdateSourceTrigger.LostFocus.
You should remove all redundant UpdateSourceTrigger.PropertyChanged from the bindings to improve readability.

Consider to use out instead of ref if you don't intend to read the variable. If you only set the value prefer to use out to hint your intent to any reader. Use in if don't intent to modify the reference (read-only reference).
Your Set method should look something like this:

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

I refactored your complete code trying to improve it:

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
  • 1
  • 4
  • 28
  • 44
  • Thank you so much @BionicCode. I will follow this approach. That was async void because it was a event handler. When I copied bits and pieces of the code I didn't update it. – nikhil May 24 '20 at 15:28
  • 1
    I am happy that I could help you. I felt like I should mention everything that could be improved. I hope you didn't get it wrong. Let me know if I can help you with this. – BionicCode May 24 '20 at 17:07
  • 1
    Thank you so much @BionicCode your post helped a lot along with the above answer. I have modified my code and it works now. – nikhil May 24 '20 at 17:12
  • Sorry @bionicCode yesterday, I didn't test properly. One case was working and I thought everything worked. so i ended up creating a new app. Please see my updated code. I am sorry for going back and forth. – nikhil May 25 '20 at 22:15
  • Thank you @BionicCode I did exactly like the way you suggested and it works. Now I tested it perfectly. Thank you so much :) – nikhil May 26 '20 at 00:16
  • Don't worry. This is still the same problem. You must understand that you can't update objects, but properties. See the first paragraph of my answer. You always have a `Binding.Source` which could be any object. You have a `Binding.Target` which binds to the source. Of course the `Binding` can't change the `Binding.Source` as this would destroy/re-define the binding. But `Binding` is passive - it can't change target or source by itself. – BionicCode May 26 '20 at 01:18
  • When you create a `Binding` and assign it to a property by using markup `{Binding ...}` or `BindingOperation.SetBinding` or `FrameworkElement.SetBinding` the `Binding is wrapped into a `BindingExpression`, which is immutable. This means it is impossible to change an active `Binding`. When you bind directly to the `DataContext` of a template e.g. `{Binding}` (which is equivalent to `{Binding Path=.}`), then the binding engine creates a `Binding` where the `Binding.Source` is set to the `DataContext`. `Binding.Path` is set to the current source. – BionicCode May 26 '20 at 01:18
  • When the binding to this `DataContext` is `TwoWay`, then a changing `Binding.Target` (e.g. `TextBox`) would have to directly change the `DataContext` which is also the `Binding.Source`. And this is not possible; again, you can't modify the `Binding.Source` (or `BindingTarget`) of an active `Binding`. A `Binding` is active when it is assigned an controlled by a `BindingExpression`. The `Binding` simply does nothing. You can only modify the referenced value of `Binding.Path`. `Binding.Path` points to property on the `Binding.Source`. – BionicCode May 26 '20 at 01:19
  • This is why I recommended to use a wrapper class like the `Parameter` class in my example. This way the `Binding.Path` doesn`t point to the `Binding.Source`. Modifying the `Path` doesn't break the binding. It may appear to be complicated, but when think about it, it makes sense. You usually bind to properties. The markup `{Binding Value}` creates a `Binding` where `Binding.Source` is set to the current `DataContext` and `Binding.Path` is set to `Value`. – BionicCode May 26 '20 at 01:20
  • You can't replace the `Binding.Source` e.g. a view model (or `int` object). But you can replace the value of the view model's property to which `Binding.Path` is pointing to. The wrapper object e.g. the `Parameter` class allows to set the `Binding.Path` property e.g. to `Parameter.Value` so that the `TwoWay` binding can modify it. – BionicCode May 26 '20 at 01:22
  • [Binding Declarations Overview: Binding Path Syntax](https://learn.microsoft.com/en-us/dotnet/framework/wpf/data/binding-declarations-overview#binding-path-syntax), [Binding Declarations Overview: Default Behaviors](https://learn.microsoft.com/en-us/dotnet/framework/wpf/data/binding-declarations-overview#default-behaviors), [Data binding overview in WPF: Specifying the path to the value](https://learn.microsoft.com/en-us/dotnet/desktop-wpf/data/data-binding-overview#specifying-the-path-to-the-value), – BionicCode May 26 '20 at 01:32
  • [Data binding overview in WPF: Binding and BindingExpression](https://learn.microsoft.com/en-us/dotnet/desktop-wpf/data/data-binding-overview#binding-and-bindingexpression) – BionicCode May 26 '20 at 01:33
  • Same applies to UWP. – BionicCode May 26 '20 at 01:34
  • Thank you @BionicCode so much for the explanation. I have been blindly doing the bindings without this knowledge. And yes as you said i should have a used a wrapper class like your parameter. In short you can't bind to objects but you can bind to the properties. So in my example lets say if the property value i.e. CurrentParameterValue is a List and I am binding it to a combobox, then I should have the same problem right, because string is also a reference type. – nikhil May 26 '20 at 03:00
  • Yes the story is the same. It doesn't matter if reference type or value type. It's about the fact that this objects are items of a collection and not values of properties. Inside the `DataTmplate` binding to the templated item will set the the `Binding.Path` to the `Binding.Source`. In this scenario modifying the value that the `Binding.Path` is pointing to would modify the `Binding.Source`. In `OneWay` or `OneTime` this doesn't matter. But for `TwoWay` this doesn't work (or the binding engine will implicitly turn the binding into a `OneWay`). – BionicCode May 26 '20 at 08:48
  • You can't change the `Binding.Source`, `Binding.Target` and `Binding.Path`, as for an active `Binding` they are managed by a `BindingExpression` which is immutable. You can only modify the value that `Binding.Path` is pointing to (but not the `Binding.Path` itself). `Binding.Path` must be a public property (a member of the opbject referenced by `Binding.Source`. – BionicCode May 26 '20 at 08:49
  • thank you so much for the explanation. I will keep this in mind when working with datatemplates or two way binding in the future. – nikhil May 26 '20 at 14:38
  • Hi @BionicCode can you please help me with this https://stackoverflow.com/questions/62687039/property-changed-is-not-updating-the-ui-from-inside-a-task – nikhil Jul 02 '20 at 00:10
1

But when I change the values in the TextBox it dosen't update back the source that is the CurrentParameterValue property.

Binding in ListView doesn't know how to update the Property of type object because it's ItemsSource and it can update only ICollection such as you can't interact with object like List in C#. for example:

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.

You need this solution then.

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

Additionally

I have no idea what do you want to achieve or solve with this syntax

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

Everything must work with this

<ListView ItemsSource="{Binding AtlasMethodParameterList}">
  • Mode=TwoWay is default Mode, you may not include it here explicitly.
  • UpdateSourceTrigger=PropertyChanged (Default is LostFocus) is needed in UI->VM direction, not in a back way. So, it's useless here. You may apply it to the TextBox in template instead.

EDIT

Because Two-way Binding requires explicit Path and the target must be a Property which contains Setter.

The workaround with your Demo app

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

Additionally you may implement INPC for the Value regarding to your needs.

aepot
  • 4,558
  • 2
  • 12
  • 24
  • Can you please help me with this new problem, I have updated my code. – nikhil May 24 '20 at 02:42
  • @nihnil the Converter does nothing. What's the purpose? – aepot May 24 '20 at 07:17
  • 1
    My bad @arpot I have been trying different ways. I have updated my code based on the change you suggest and from the guidelines explained in the below post and it works just fine. – nikhil May 24 '20 at 17:13
  • Sorry @aepot yesterday, I didn't test properly. One case was working and I thought everything worked. so i ended up creating a sample app. I am sorry for going back and forth. Please see my updated code. – nikhil May 25 '20 at 22:14
  • @nikhil added **EDIT** to the answer. Next time please create a separate question. And don't remove old text from the question to save the history but append the edits. – aepot May 25 '20 at 23:47
  • Sure sorry about that. i will append the edits next time. – nikhil May 25 '20 at 23:53
  • Thank you so much, like @BionicCode has suggested. Thanks so much for the help. Let me try it in my main app. I have done so in most parts of my code like the way you said I wonder how I missed the basics. – nikhil May 26 '20 at 00:03
  • 1
    Thank you @aePot. It works just fine even with the earlier code I posted. I have tested thoroughly this time around. – nikhil May 26 '20 at 00:17
  • If the `Binding.Source` is not a `DependencyProperty`, then `INotifyPropertyChanged` is mandatory for every `BindingMode` other than `OneWayToSource` or `OneTime`. _"But List's item is just a Field, not Property: no Setter, no updates."_ `List` items are strings i.e objects and not fields. The correct explanation is that you can't change the `Binding.Source` (the template's `DataContext` e.g. the `int` object). You can only change the value of a _property_ on this source, which is declared by `Binding.Path`. Your solution is absolutely correct, but the explanation is not. – BionicCode May 26 '20 at 01:48
  • @BionicCode removed part of explanation. But didn't understand the correct one. And no ideas how to google it, can you give me some link or something more to learn about? – aepot May 26 '20 at 06:45
  • 1
    You can't change the `Binding.Source`, `Binding.Target` and `Binding.Path`, as an active `Binding` is managed by a `BindingExpression` which is immutable. `Binding`is passive meta data. `BindingExpression is actually doing the binding magic. Once setup (with a `Binding` instance) it can't be changed. Modifying the `Binding.Source` directly would requireto modify the immutable `BindingExpression`. You can only modify the value that `Binding.Path` is pointing to (but not the `Binding.Path` itself). `Binding.Path` must be a public property (a member of the object referenced by `Binding.Source`). – BionicCode May 26 '20 at 09:41
  • I don't know a resources where this is explicitly stated. But you can read it between the lines. Once you know how bindings work it's logical. It's also experience. I once wrote a custom invert markup extension which accepts a binding expression to intercept and invert a value of any type `TwoWay`. I had to learn that you can't modify an active ´Binding` once controlled by a `BindingExpression`. This would throw an `InvalidOperationException` something like _"Can't change the binding after it has been used"_. – BionicCode May 26 '20 at 09:41
  • And used means wrapped by a `BindingExpression` which is happening under the hood when the `Binding` markup extension is evaluated by the XAML parser or the binding is setup using `BindingOperation.SetBinding` or `FrameworkElement.SetBinding`. You can try it yourself. – BionicCode May 26 '20 at 09:42
  • 1
    I recommend [Binding Sources Overview: https://learn.microsoft.com/en-us/dotnet/framework/wpf/data/binding-sources-overview#other-characteristics](https://learn.microsoft.com/en-us/dotnet/framework/wpf/data/binding-sources-overview#other-characteristics) (whole page is good), [Data binding overview in WPF: Binding and BindingExpression](https://learn.microsoft.com/en-us/dotnet/desktop-wpf/data/data-binding-overview#binding-and-bindingexpression) (whole page is a good read), – BionicCode May 26 '20 at 09:42
  • 1
    [Optimizing Performance: Data Binding](https://learn.microsoft.com/en-us/dotnet/framework/wpf/advanced/optimizing-performance-data-binding) (VERY GOOD). – BionicCode May 26 '20 at 09:42
  • Hi @aepot can you please help me with this, https://stackoverflow.com/questions/62685730/property-changed-is-not-being-triggered-inside-a-task I think I am missing something very simple here. – nikhil Jul 01 '20 at 21:43