Is it possible to avoid rebinding when object is renewed?

Emon Haque 3,176 Reputation points
2021-04-30T02:31:07.2+00:00

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 Presentation Foundation
Windows Presentation Foundation
A part of the .NET Framework that provides a unified programming model for building line-of-business desktop applications on Windows.
2,681 questions
{count} votes

Accepted answer
  1. Emon Haque 3,176 Reputation points
    2021-05-01T14:52:32.51+00:00

    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.

    0 comments No comments

0 additional answers

Sort by: Most helpful