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:
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.