C# - Notifying the parent property when one of the child properties changes - c#

I have a class with a property 'DesignParameters' that upon changing, would affect another property called 'AxialMomentDataLists'. However, the 'DesignParameters' is comprised of a bunch of other 'child' properties that are accessible through a datagrid on the UI and also implement property changed. If one of the child properties changes, I also want 'DesignParameters' to automatically update, which would in-turn call for a new 'AxialMomentDataLists' to be set. Does anyone have advice on the best method to achieve this?
public class Column : ObservableObject
{
private double length;
private DesignParameters desParameters;
public DesignParameters DesParameters
{
get { return desParameters; }
set
{
desParameters = value;
RaisePropertyChanged(nameof(DesParameters));
RaisePropertyChanged(nameof(AxialMomentDataLists));
}
}
public List<AxialMomentDataSet> AxialMomentDataLists
{
get { return CalculateAxialMomentLists(ColumnForce, DesParameters); }
set { }
}
}
Excerpt from child class:
public class DesignParameters : ObservableObject
{
#region Private variables
private double phiC;
private double phiS;
private int cover;
private int reinforcementYieldStrength;
private int concreteStrength;
private double b;
private double h;
private LiveLoadReductionType liveLoadReduction;
private StirrupType stirrupBar;
private int numberOfXBars;
private int numberOfYBars;
private BarDiameterType longitudinalBarDiameter;
private double longitudinalReinforcementPercentage;
List<Bar> rebar;
#endregion
public int NumberOfXBars
{
get { return numberOfXBars; }
set
{
numberOfXBars = PropertyMethods.SetNumberOfBars("x", value, B, H, Cover, LongitudinalBarDiameter);
RaisePropertyChanged(nameof(NumberOfXBars));
RaisePropertyChanged(nameof(Rebar));
RaisePropertyChanged(nameof(LongitudinalReinforcementPercentage));
RaisePropertyChanged(nameof(AxialResistance));
}
}
}
EDIT
I have created a more simple piece of code that tries to accomplish approximately the same thing that I am here (https://github.com/dtnaughton/SampleApp)
Basically, I had a UI that binds to properties on the FamilyMember and the properties Age and Name can be changed. On the Family (parent) class, I have a property CumulativeFamilyAge which returns a method that basically sums up the age of the family member (in this example, there is only 1 family member and therefore it should read the same as the FamilyMember.Age.
When the user changes FamilyMember.Age on the UI, I want Family to detect that one of it's child properties has changed, and therefore update CumulativeFamilyAge accordingly.

For example, if the 'NumberOfXBars' property gets changed (it is exposed on the UI) then I need the parent property 'DesignParameters' on the Column class to recognize one of it's child properties has changed and therefore this must update.
I'll write as I understand it.
In the Column class, you have a DesParameters property that contains an instance of DesignParameters.
If the value of one of the properties (NumberOfXBars) of this instance has changed, then you want to update all views associated with this instance.
Here you have an obvious problem with breaking the contract of the INotifyPropertyChanged interface. According to this contract, the PropertyChanged event does not fire when a value is assigned to a property, but only if this value is different from the previous one.
For this reason, if you just raise this event for the same value, then the bindings will not react to this.
As I think, you are simply not thinking correctly.
You don't need to update the bindings to the instance.
You need to update any bindings to any of its properties.
For this, the interface contract provides for the creation of an event with an empty argument.
If the above is a correct understanding of your problem, then try this implementation:
public int NumberOfXBars
{
get { return numberOfXBars; }
set
{
numberOfXBars = PropertyMethods.SetNumberOfBars("x", value, B, H, Cover, LongitudinalBarDiameter);
RaisePropertyChanged(string.Empty);
}
}
Example implementation after additional question:
I have created a more simple piece of code that tries to accomplish approximately the same thing that I am here (https://github.com/dtnaughton/SampleApp)
Improved implementation of Person entity using generic MVVMLight methods.
using GalaSoft.MvvmLight;
namespace SampleApp.Models
{
public class Person : ObservableObject
{
private string _name;
private int _age;
public string Name { get => _name; set => Set(ref _name, value); }
public int Age { get => _age; set => Set(ref _age, value); }
public Person()
{
Name = "TestName";
Age = 30;
}
}
}
Family is derived from ViewModelBase to be able to watch event properties change.
This is done to demonstrate how to observe changes in entities in properties.
Also added a second Person entity to improve the demo example.
using GalaSoft.MvvmLight;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace SampleApp.Models
{
public class Family : ViewModelBase
{
private Person _familyMember;
private int _cumulativeFamilyAge;
private Person _childMember;
public Person FamilyMember { get => _familyMember; set => Set(ref _familyMember, value); }
public Person ChildMember { get => _childMember; set => Set(ref _childMember, value); }
public int CumulativeFamilyAge { get => _cumulativeFamilyAge; private set => Set(ref _cumulativeFamilyAge, value); }
public Family()
{
FamilyMember = new Person() { Name = "Father" };
ChildMember = new Person() { Name = "Son", Age = 7 };
}
public override void RaisePropertyChanged<T>([CallerMemberName] string propertyName = null, T oldValue = default, T newValue = default, bool broadcast = false)
{
base.RaisePropertyChanged(propertyName, oldValue, newValue, broadcast);
if (propertyName == nameof(FamilyMember) || propertyName == nameof(ChildMember))
{
if (oldValue is Person oldPerson)
oldPerson.PropertyChanged -= OnPersonPropertyChanged;
if (newValue is Person newPerson)
newPerson.PropertyChanged += OnPersonPropertyChanged;
CalculateCumulativeFamilyAge();
}
}
private void OnPersonPropertyChanged(object sender, PropertyChangedEventArgs e)
{
CalculateCumulativeFamilyAge();
}
public void CalculateCumulativeFamilyAge()
{
CumulativeFamilyAge = (FamilyMember?.Age ?? 0)
+ (ChildMember?.Age ?? 0);
return;
}
}
}
The Data Context of the Window is best defined in XAML - this makes XAML design much easier.
If you need to have a field in Code Behind with a link to the ViewModel, you should get it from XAML.
using SampleApp.ViewModels;
using System.Windows;
namespace SampleApp
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private readonly MainViewModel viewModel /*= new MainViewModel()*/;
public MainWindow()
{
InitializeComponent();
//DataContext = viewModel;
viewModel = (MainViewModel)DataContext;
}
}
}
Added a DataTemplate for the Person entity in the Window and set the output for both entities.
<Window x:Class="SampleApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewmodels="clr-namespace:SampleApp.ViewModels"
xmlns:local="clr-namespace:SampleApp.Models"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.DataContext>
<viewmodels:MainViewModel/>
</Window.DataContext>
<Window.Resources>
<DataTemplate DataType="{x:Type local:Person}">
<Border Margin="5" Background="LightSkyBlue" Padding="5">
<StackPanel>
<TextBox Text="{Binding Name}" Height="30"/>
<TextBox Text="{Binding Age}" Height="30"/>
</StackPanel>
</Border>
</DataTemplate>
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<ContentPresenter Content="{Binding FamilyModel.FamilyMember}"/>
<ContentPresenter Content="{Binding FamilyModel.ChildMember}"/>
<TextBlock Text="{Binding FamilyModel.CumulativeFamilyAge}" Height="30"/>
</StackPanel>
</Grid>
</Window>

I am not sure if I unterstood you correctly. DesignParameters implements INotifyPropertyChanged and whenever one of its properties changes, you want to invoke PropertyChanged in your Column class for the AxialMomentDataLists property?
If so, this is quite easy. Just subscribe to this event whenever you set a new value to your DesParameters property. Don't forget to unsubscribe the event from the old value. A null check might be necessary (or do you use C# 8 with nullable reference types?)
public class Column : ObservableObject
{
private double length;
private DesignParameters desParameters;
public DesignParameters DesParameters
{
get { return desParameters; }
set
{
if(desParameters != null)
{
desParameters.PropertyChanged -= DesParametersChildPropertyChanged;
}
desParameters = value;
desParameters.PropertyChanged += DesParametersChildPropertyChanged;
RaisePropertyChanged(nameof(DesParameters));
RaisePropertyChanged(nameof(AxialMomentDataLists));
}
}
private void DesParametersChildPropertyChanged(object sender, PropertyChangeEventArgs args)
{
RaisePropertyChanged(nameof(DesParameters));
RaisePropertyChanged(nameof(AxialMomentDataLists));
}
public List<AxialMomentDataSet> AxialMomentDataLists
{
get { return CalculateAxialMomentLists(ColumnForce, DesParameters); }
set { }
}
}

Related

WPF component content not updating after view model change

I have the following view model:
public class ViewModel : ObservableObject
{
public string Watermark { get; set; } = "Loading...";
}
where ObservableObject is from Microsoft.Toolkit.Mvvm.ComponentModel which implements INotifyPropertyChanged and INotifyPropertyChanging.
I have the following XAML:
<xctk:WatermarkTextBox>
<xctk:WatermarkTextBox.WatermarkTemplate>
<DataTemplate>
<ContentControl Content="{Binding DataContext.Watermark, RelativeSource={RelativeSource AncestorType=Window}}"></ContentControl>
</DataTemplate>
</xctk:WatermarkTextBox.WatermarkTemplate>
</xctk:WatermarkTextBox>
And lastly the following C# code:
private async void Window_Loaded(object sender, RoutedEventArgs e)
{
Dispatcher.Invoke(() =>
{
// Assigning property of textbox directly with .Watermark = "" does not work
// Updating view model property doesn't work either
Model.Watermark = "Done";
});
}
When the window loaded method gets called however, the watermark of the watermark textbox does not update to reflect its supposed new value, "Done".
It might be a lack of general WPF understanding but if anyone could give a pointer as to why this might fail I would greatly appreciate it.
Thanks!
You aren't using ObservableObject properly. It's not magic (unlike some solutions that use frameworks such as Fody), so you still have to do the work yourself to propagate changes. Note the example in the documentation.
public class User : ObservableObject
{
private string name;
public string Name
{
get => name;
set => SetProperty(ref name, value);
}
}
So you'll have to follow that pattern:
public class ViewModel : ObservableObject
{
private string watermark;
public ViewModel()
{
Watermark = "Loading...";
}
public string Watermark
{
get => watermark;
set => SetProperty(ref watermark, value);
}
}

WPF OneWay binding works only once

I have a View that have two comboboxes. One is where user selects routing pipe type name, and the other where there should be a list of available diameters for the chosen pipe type.
Whenever user selects the pipe type, the other combobox should update the list of available diameters.
AvailableDiameters and RoutingPipeTypeName properties are static in Context class, that implements INotifyPropertyChanged interface. In xaml I've set the bindings to these properties, in code behind also the DataContext.
The problem is that the list of diameters get's updated only once, when the view is initialized.
When debugging I can see that the properties backing field's values are updated properly when selection on pipe type name is changed, only in the UI the available diameters list is not updated...
Context class:
public class Context : INotifyPropertyChanged
{
public static Context This { get; set; } = new Context();
public static string RoutingPipeTypeName
{
get => _routingPipeTypeName;
set
{
if (_routingPipeTypeName != value)
{
_routingPipeTypeName = value;
This.OnPropertyChanged(nameof(RoutingPipeTypeName));
}
}
}
public static List<double> AvailableDiameters
{
get => _availableDiameters;
set
{
//check if new list's elements are not equal
if (!value.All(_availableDiameters.Contains))
{
_availableDiameters = value;
This.OnPropertyChanged(nameof(AvailableDiameters));
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
xaml:
<ComboBox Width="80" SelectedValue="{Binding Path=RoutingPipeTypeName, Mode=OneWayToSource}">
<ComboBoxItem Content="Example pipe type 1"></ComboBoxItem>
<ComboBoxItem Content="Example pipe type 2"></ComboBoxItem>
</ComboBox>
<ComboBox Width="80" SelectedValue="{Binding Path=RoutingDiameter, Mode=OneWayToSource}" ItemsSource="{Binding Path=AvailableDiameters, Mode=OneWay}">
</ComboBox>
code behind:
public Context AppContext => Context.This;
public MyView()
{
InitializeComponent();
Instance = this;
DataContext = AppContext;
}
And the client class that is responsible for updating the list of diameters:
public void InitializeUIContext()
{
Context.This.PropertyChanged += UIContextChanged;
if (Cache.CachedPipeTypes.Count > 0)
Context.RoutingPipeTypeName = Cache.CachedPipeTypes.First().Key;
}
private void UIContextChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(Context.RoutingPipeTypeName))
{
Context.AvailableDiameters = Cache.CachedPipeTypes.First().Value.GetAvailableDiameters();
}
}
I expected such set-up would update the diameters combobox each time the selection is changed on the pipe types property.
Instead it updates it only once, when the view is initialized... Why?
Do not use static properties for binding to an object (which you have correctly passed to the DataContext of your view).
Declare the properties without the static modifier and replace This.OnPropertyChanged by OnPropertyChanged:
public string RoutingPipeTypeName
{
get => _routingPipeTypeName;
set
{
if (_routingPipeTypeName != value)
{
_routingPipeTypeName = value;
OnPropertyChanged(nameof(RoutingPipeTypeName));
}
}
}
You should also remove the static This from your Context class and simply write
public Context AppContext { get; } = new Context();

Issue with databinding to class?

I'm having an issue with databinding a textblock to a custom prop inside another class, what am I doing wrong?
mainpage:
public partial class MainPage : PhoneApplicationPage
{
// Constructor
public MainPage()
{
InitializeComponent();
InitializeLanguage();
}
private void InitializeLanguage()
{
LanguageHelper lh = new LanguageHelper();
// this.TitlePanel.DataContext = lh;
txtTitle.DataContext = lh;
}
}
databinding:
<TextBlock x:Name="txtTitle"
Text="{Binding homepage_subheading}"
Style="{StaticResource PhoneTextNormalStyle}"
Foreground="White"
Margin="12,0"/>
LanguageHelper class:
public class LanguageHelper
{
public String homepage_subheading;
public void changeLanguage()
{
if (true)
{
//english
homepage_subheading = "This is the top / sub Heading";
}
}
}
You don't have a property but a public field, and the databinding engine only works on properties.
So you need to change your class:
public String homepage_subheading { get; set; }
If you want to also notify the UI with your changes of your properties your LanguageHelper should implement the INotifyPropertyChanged interface and fire the PropertyChange event when you modify your properties.
You should declare some dependency property or use INotifyPropertyChanged, I would like to use a dependency property:
public class LanguageHelper : DependencyObject {
public static DependencyProperty Hompage_subheadingProperty =
DependencyProperty.Register("Homepage_subheading", typeof(string), typeof(LanguageHelper));
public String Homepage_subheading {
get { return (string) GetValue(Homepage_subheadingProperty);}
set { SetValue(Homepage_subheadingProperty, value);}
}
}
Note about the naming convention in C#, all properties should have first letter capitalized.

Why wont UI update on RaisePropertyChanged?

I have a static IList which acts as a repository in a static class:
//static class Settings
public static IList RecentSearchedRepo = new ObservableCollection<object>();
and an IList located in another class which I bind my UI grid to :
//component class
private IList recentsearch = new ObservableCollection<object>();
public IList RecentSearch
{
get
{
return recentsearch;
}
set
{
recentsearch = value;
RaisePropertyChanged("RecentSearch");
}
}
I add objects to RecentSearchedRepo :
RecentSearchedRepo.add(searchitem)
then set RecentSearch to the static list
RecentSearch = Settings.RecentSearchedRepo;
XAML snippet:
<GridLayout:RecentSearchGrid x:Name="recentSearchGrid" ItemsSource="{Binding RecentSearch}" />
snippet from RecentSearchGrid class which extends UserControl:
public IList ItemsSource
{
get
{
return GetValue(ItemsSourceProperty) as IList;
}
set
{
SetValue(ItemsSourceProperty, value);
}
}
private static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register("ItemsSource", typeof(IList), typeof(RecentSearchGrid), new PropertyMetadata(null, OnItemsSourcePropertyChanged));
private static void OnItemsSourcePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
RecentSearchGrid source = d as RecentSearchGrid;
if (source != null)
{
source.setListforgrid(source.ItemsSource);
}
}
The problem is when I add the first item to RecentSearchedRepo the UI is updated , but on every subsequent add the UI does not update.
Instead of:
RecentSearch = Settings.RecentSearchedRepo;
Try doing:
RecentSearch.Clear();
var freshData = Settings.RecentSearchedRepo;
if (freshData != null)
foreach (var item in freshData)
RecentSearch.Add(item);
You were killing the binding by reassigning the reference.
EDIT: After yours
You're doing it backwards: that OnItemsSourcePropertyChanged shouldn't be setting the source, it shouldn't be there at all actually.
You must bind, in RecentSearchGrid.xaml, to the ItemsSource dependency property declared in RecentSearchGrid.xaml.cs
I don't think that there's enough information here to answer your question. The following simple application mirrors the scenario that I see described in the question and it works as expected:
// MySettings.cs
public static class MySettings
{
public static IList RecentSearchedRepo = new ObservableCollection<object>();
}
// MyVm.cs
public class MyVm : INotifyPropertyChanged
{
private IList recentSearch = new ObservableCollection<object>();
public event PropertyChangedEventHandler PropertyChanged;
public MyVm()
{
this.RecentSearch = MySettings.RecentSearchedRepo;
}
public IList RecentSearch
{
get { return recentSearch; }
set
{
recentSearch = value;
this.RaisePropertyChanged("RecentSearch");
}
}
private void RaisePropertyChanged(string p)
{
if (this.PropertyChanged != null) this.PropertyChanged(this, new PropertyChangedEventArgs(p));
}
}
// MainWindow.xaml.cs
public partial class MainWindow : Window
{
public MainWindow()
{
this.InitializeComponent();
// Initialization as described in the question
MySettings.RecentSearchedRepo.Add("SearchItem1");
MySettings.RecentSearchedRepo.Add("SearchItem2");
this.DataContext = new MyVm();
}
private void Button_Click_1(object sender, RoutedEventArgs e)
{
// Add a new item later
MySettings.RecentSearchedRepo.Add("NewlyAddedSearchItem");
}
}
// MainWindow.xaml
<Window x:Class="ScratchWpf.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<DockPanel>
<Button DockPanel.Dock="Bottom" Content="Add new Search Item" Click="Button_Click_1" />
<ListBox ItemsSource="{Binding RecentSearch}" />
</DockPanel>
</Window>
I'm going to try putting on my psychic hat and ask if, perhaps, you are adding additional items to the wrong collection. Does the collection get recreated and placed at the binding after a single item is added, but later items are added to the original collection instead of the new one?
Given that you stated RecentSearchGrid is a UserControl, we can also infer that the implementation of ItemsSource may be a custom one rather than the standard one that would be inherited from an ItemsControl. It's possible that the RecentSearchGrid is breaking the binding incorrectly in there somehow.
I agree with Baboon. What is the purpose of OnItemsSourcePropertyChanged? In a typical implementation, I wouldn't expect that to be there.
The problem may be as follow:
OnItemsSourcePropertyChanged will not get called, if the instance does not change.
From WPF point of view, when you RaisePropertyChangeEvent, but the instance of the bound collection does not change, PropertyChange handler will not be called at all.
Is Settings.RecentSearchedRepo the same instance through the lifetime of the app?

Using ListBox to navigate a TextBox with MVVM

I am trying to move the following function listView_SelectionChanged away from code-behind and handle it directly inside my ViewModel (or directly as XAML). And I was hoping that someone might have a better idea on how to implement this.
The TextBox contains Sections e.g. [Secion1] and to help navigate I have a ListBox on the side of the TextBox that contains a list of all Sections. If you click on one of the Sections it will automatically jump to that part of the Text.
The code currently looks something like this:
XAML
ListBox ItemsSource="{Binding Path=Sections}" Name="listBox"
SelectionMode="Single" Width="170"
DisplayMemberPath="Section"
SelectionChanged="listView_SelectionChanged"/>
<TextBox Name="TextBox1" Text="{Binding Path=Source}"/>
Model
public class SourceData
{
public SourceData()
{
Sections = new List<SectionData>();
}
public String Source { get; set; }
public List<SectionData> Sections { get; set; }
}
public class SectionData
{
public int Line { get; set; } // Line of the Section
public String Section { get; set; } // Section name (e.g. [Section1]
}
Code-behind
private void listView_SelectionChanged(object sender,
System.Windows.Controls.SelectionChangedEventArgs e)
{
var test = (SectionData)listBox.SelectedItem; // Get Information on the Section
if (test.Line > 0 && test.Line <= TextBox1.LineCount) // Validate
{
TextBox1.ScrollToLine(test.Line - 1); // Scroll to Line
}
}
In such situations I usually create an attached behavior (in your case it will be a behavior which will allow synchronizing textbox scrolled line), add property in ViewModel (SourceData) which will rule that attached behavior and bind behavior to this property.
Steps you should do in your case (I assume you know how to create an attached properties):
1) Create attached behavior ScrolledLine for textbox. It should support at least one-way binding. In attached property callback you will scroll textBox (to which behavior is attached) to the line. Below you will find a quick sample how to implement such a behavior.
2) Your SourceData should be extended with at least two properties: SelectedSection and ScrolledLine. ScrolledLine should be raising PropertyChanged. SelectedSection setter should change ScrolledLine:
private SectionData _selectedSection;
public SectionData SelectedSection
{
get { return _selectedSection; }
set
{
_selectedSection = value;
if (_selectedSection != null) SelectedLine = _selectedSection.Line;
}
}
3) Bind your view to these two new properties:
b below is xml-namespace for your attached behavior from #1
<ListBox ItemsSource="{Binding Path=Sections}" SelectionMode="Single" Width="170" DisplayMemberPath="Section" SelectedItem="{Binding SelectedSection, Mode=TwoWay}" />
<TextBox Text="{Binding Path=Source}" b:Behaviors.ScrolledLine="{Binding ScrolledLine}" />
4) Remove your listView_SelectionChanged event handler from view. Your view should not have any code except InitializeComponent from now on.
P.S.: Here is a sample how your attached behavior should look like:
public class b:Behaviors
{
#region Attached DP registration
public static int GetScrolledLine(TextBox obj)
{
return (int)obj.GetValue(ScrolledLineProperty);
}
public static void SetScrolledLine(TextBox obj, int value)
{
obj.SetValue(ScrolledLineProperty, value);
}
#endregion
public static readonly DependencyProperty ScrolledLineProperty=
DependencyProperty.RegisterAttached("ScrolledLine", typeof(int), typeof(Behaviors), new PropertyMetadata(ScrolledLine_Callback));
// This callback will be invoked when 'ScrolledLine' property will be changed. Here you should scroll a textbox
private static void ScrolledLine_Callback(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
var textbox = (TextBox) source;
int newLineValue = (int)e.NewValue;
if (newLineValue > 0 && newLineValue <= textBox.LineCount) // Validate
textbox.ScrollToLine(newLineValue - 1); // Scroll to Line
}
}

Categories