question

EmonHaque-1485 avatar image
0 Votes"
EmonHaque-1485 asked EmonHaque-1485 answered

Is it possible to avoid rebinding when object is renewed?

In previous version of my App, I used UserControl as View and set binding in UserControl.xaml. With that approach, when I renew an object in ViewModel, I just have to call OnPropertyChanged(nameof(theObject)) and the UI updates automatically. Now, as I'm doing everything with FrameworkElement and in related.cs file instead of xaml, I've noticed a difference. Here is my base Add viewmodel:

 public abstract class AddBase<T> : Notifiable where T : /*IInsertable,*/ new()
 {
     public T TObject { get; set; }
     public Action Add { get; set; }
     public AddBase() {
         TObject = new T();
         Add = addTObject;
     }
     protected abstract ObservableCollection<T> collection { get; }
     protected abstract void insertInDatabase();
     protected abstract void renewTObject();
     void addTObject() {
         lock (SQLHelper.key) { insertInDatabase(); }
         collection.Add(TObject);
         renewTObject();
         OnPropertyChanged(nameof(TObject));
     }
 }

All viewmodels, that needs add functionality, implements this base class and on click of a button, addTObject method is executed. Here's the AddPlot view:

 class AddPlot : CardView
 {
     public override string Header => "Plot";
     EditText name, description;
     CommandButton button;
     AddPlotVM viewModel;
     public AddPlot() : base() {
         viewModel = new AddPlotVM();
         initializeUI();
         bind();
         viewModel.PropertyChanged += (s, e) => bind();
     }
     void initializeUI() {
         name = new EditText() { Hint = "Name", IsRequired = true, Icon = Icons.Plot };
         description = new EditText() { Hint = "Description", IsMultiline = true, IsRequired = true, MaxLength = 100, Icon = Icons.Description };
         button = new CommandButton() { WidthAndHeight = 24, Icon = Icons.Add, Command = viewModel.Add, HorizontalAlignment = HorizontalAlignment.Right };
         Grid.SetRow(description, 1);
         Grid.SetRow(button, 2);
         var grid = new Grid() {
             RowDefinitions = {
                 new RowDefinition() { Height = GridLength.Auto },
                 new RowDefinition(),
                 new RowDefinition() { Height = GridLength.Auto },
             },
             Children = { name, description, button }
         };
         base.setContent(grid);
     }
     void bind() {
         DataContext = viewModel.TObject;
         name.SetBinding(EditText.TextProperty, new Binding(nameof(Plot.Name)));
         name.SetBinding(EditText.ErrorProperty, new Binding(nameof(Plot.ErrorName)));
         description.SetBinding(EditText.TextProperty, new Binding(nameof(Plot.Description)));
         description.SetBinding(EditText.ErrorProperty, new Binding(nameof(Plot.ErrorDescription)));
         button.SetBinding(IsEnabledProperty, new Binding(nameof(Plot.IsValid)));
     }
 }

and the AddPlotVM implements the base viewmodel. CardView is a FrameworkElement with a single child, Card, and with base.setContent(grid); in the initializeUI method I add content in the Card. With all these in AddPlot view it works as expected. If I remove viewModel.PropertyChanged += (s, e) => bind(); in the constructor the UI doesn't update! The top left Card is the View and here's what it does with and without that:

92795-new.gif

So with that in constructor when I input text in name and description and hit space after navigating, with TAB, to the button, the content of both EditText becomes null as the TObject in the the viewmodel is renewed and button gets disabled. Without that, TObject is renewed when I hit space on the button but none of the EditText and CommandButton updates!

Is it a must to rebind when TObject is renewed or there's some other function/property that does that automatically?


EDIT

This might also be relevant in this case, In EditText I've a TextBox, inputBox, and a TextBlock, errorBlock, to type text and display error and following two dependency properties for binding:

 public string Text {
     get { return (string)GetValue(TextProperty); }
     set { SetValue(TextProperty, value); }
 }

 public string Error {
     get { return (string)GetValue(ErrorProperty); }
     set { SetValue(ErrorProperty, value); }
 }

 public static readonly DependencyProperty TextProperty =
     DependencyProperty.Register("Text", typeof(string), typeof(EditText), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));

 public static readonly DependencyProperty ErrorProperty =
     DependencyProperty.Register("Error", typeof(string), typeof(EditText), new PropertyMetadata(null, onErrorChanged));

 static void onErrorChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
     var o = d as EditText;
     var err = e.NewValue == null ? null : e.NewValue.ToString();
     if (err == string.Empty) {
         o.invalidIcon.Visibility = Visibility.Hidden;
         o.validIcon.Visibility = Visibility.Visible;
         if (o.MaxLength == 0) {
             o.infoContainer.Visibility = Visibility.Collapsed;
             o.separator.Visibility = Visibility.Collapsed;
         }
     }
     else {
         o.invalidIcon.Visibility = Visibility.Visible;
         o.validIcon.Visibility = Visibility.Hidden;
         if (o.MaxLength == 0) {
             o.infoContainer.Visibility = Visibility.Visible;
             o.separator.Visibility = Visibility.Visible;
         }
         if(err == null) o.OnPreviewLostKeyboardFocus(null);
     }
 }

At the end of the constructor of EditText, I've Loaded += bind; and the bind function does this:

 void bind(object sender, RoutedEventArgs e) {
     var inputBinding = new Binding() {
         Path = new PropertyPath(nameof(Text)),
         RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(EditText), 1),
         UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged,
         //Mode = BindingMode.TwoWay
     };
     inputBox.SetBinding(TextBox.TextProperty, inputBinding);
     //BindingOperations.SetBinding(inputBox, TextBox.TextProperty, inputBinding);
     var errorBinding = new Binding() {
         Path = new PropertyPath(nameof(Error)),
         RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(EditText), 1),
         //Mode = BindingMode.TwoWay
     };
     errorBlock.SetBinding(TextBlock.TextProperty, errorBinding);
     //BindingOperations.SetBinding(errorBlock, TextBlock.TextProperty, errorBinding);
 }

To test according to @DaisyTian-MSFT suggestion, I've tried with the BindingOperations both in EditText and AddPlot view:

 void bind() {
     DataContext = viewModel.TObject;
     BindingOperations.SetBinding(name, EditText.TextProperty, new Binding(nameof(Plot.Name)) { Mode = BindingMode.TwoWay });
     BindingOperations.SetBinding(name, EditText.ErrorProperty, new Binding(nameof(Plot.ErrorName)) { Mode = BindingMode.TwoWay });
     BindingOperations.SetBinding(description, EditText.TextProperty, new Binding(nameof(Plot.Description)) { Mode = BindingMode.TwoWay });
     BindingOperations.SetBinding(description, EditText.ErrorProperty, new Binding(nameof(Plot.ErrorDescription)) { Mode = BindingMode.TwoWay });
     BindingOperations.SetBinding(button, IsEnabledProperty, new Binding(nameof(Plot.IsValid)) { Mode = BindingMode.TwoWay });

     //name.SetBinding(EditText.TextProperty, new Binding(nameof(Plot.Name)));
     //name.SetBinding(EditText.ErrorProperty, new Binding(nameof(Plot.ErrorName)));
     //description.SetBinding(EditText.TextProperty, new Binding(nameof(Plot.Description)));
     //description.SetBinding(EditText.ErrorProperty, new Binding(nameof(Plot.ErrorDescription)));
     //button.SetBinding(IsEnabledProperty, new Binding(nameof(Plot.IsValid)));
 }

BUT those doesn't work! I've to keep viewModel.PropertyChanged += (s, e) => bind(); in the constructor of AddModel view to make it work.

windows-wpf
new.gif (894.7 KiB)
· 3
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

@EmonHaque-1485
Could you use below code to bind the data to check if it work for you or not?

 BindingOperations.SetBinding(Name, ErrorProperty, new Binding()
             {
                 UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged,
                 Path = new PropertyPath("Error"),
                 Mode = BindingMode.TwoWay
             }
            );
1 Vote 1 ·

@DaisyTian-MSFT, doesn't work! I've edited the post, see the EDIT part at the bottom.

0 Votes 0 ·

@DaisyTian-MSFT, one way that works is viewModel.PropertyChanged += (s, e) => DataContext = viewModel.TObject;

0 Votes 0 ·

1 Answer

EmonHaque-1485 avatar image
0 Votes"
EmonHaque-1485 answered

The problem was in the nameof(...) in binding, it doesn't return fully qualified name! So either with this:

 void bind() {
     DataContext = viewModel;
     name.SetBinding(EditText.TextProperty, new Binding("TObject.Name"));
     name.SetBinding(EditText.ErrorProperty, new Binding("TObject.ErrorName"));
     description.SetBinding(EditText.TextProperty, new Binding("TObject.Description"));
     description.SetBinding(EditText.ErrorProperty, new Binding("TObject.ErrorDescription"));
     button.SetBinding(IsEnabledProperty, new Binding("TObject.IsValid"));
 }

or 2 concatenated nameof in the constructor of Binding, I don't need viewModel.PropertyChanged += (s, e) => bind(); in constructor. Figured it out after converting both model and viewmodel as DependencyObject instead of INotifyPropertyChanged. Hopefully, this will work with INPC as well.

5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.