So I've been sifting through similar questions on SO for a few days now. I just want to know why this problem occurs. I've got a Class with properties and a SeriesCollection used for Live Charts, which are binded to the UI. Since the properties need to be able to be Serialized, the SeriesCollection can not be part of the specific modelview (but need to binded to UI for plotting a chart). like so:
public class DLVOModel
{
public SeriesCollection table { get; set; }
public DLVOConfiguration DLVOConfiguration { get; set; }
}
public partial class DLVOModelizer : Window
{
public DLVOModel model { get; set; }
public DLVOModelizer()
{
InitializeComponent();
model = CreateModel();
DataContext = model; //Databinding
}
private DLVOModel CreateModel() => new DLVOModel()
{
DLVOConfiguration = new DLVOConfiguration(),
table = new SeriesCollection(),
};
public class DLVOConfiguration
{
public double HMax { get; set; }
public int Resolution { get; set; }
//... about 25 properties
}
`
XAML:
<window>
<lvc:CartesianChart Series="{Binding Path=table}"/>
<GroupBox DataContext="{Binding DLVOConfiguration}">
<Grid>
<TextBox Text="{Binding Path=HMax, Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged}"/>
<TextBox Text="{Binding Path=Resolution, Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged}"/>
</Grid>
</GroupBox>
So this all works well untill I try to deserialize an xml file. The model gets updated properly but the UI falls behind. The textboxxes updates to the models value when I try to change their text. This is odd because:
The Databinding works fine, so UI should update immediately.
When entering a new value in the UI the model property should change, not the UI.
(Also tried a version without UpdateSourceTrigger).
Before I binded directly to the DLVOConfiguration and everything worked fine.
I know there is a way in which your modelview inherits from INotifyPropertyChanged, but for some reason I run into the same problem.
EDIT:
I added code for the case in which I used INotifyPropertyChanged from this question:
WPF DataBinding not updating?
public class DLVOConfiguration : INotifyPropertyChanged
{
private double _HMax;
public double HMax
{
get { return _HMax; }
set
{
_HMax = value;
NotifyPropertyChanged("HMax");
}
}
private int _Resolution;
public int Resolution
{
get { return _Resolution; }
set
{
_Resolution = value;
NotifyPropertyChanged("Resolution");
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
I guess you're replacing the instance being bound to somewhere. That breaks the data binding. As long as you're merely updating the properties with new values, it should work just fine.
Related
Summary
I've got an element within a data template, that I want bound to some property of the main data context.
I realise that in this specific situation, a different solution may be preferable (and I have a working solution that avoids this), but I suspect this kind of problem may come up again and I want to know how to solve it in the general case.
Below are the specifics of my situation.
The Details
Data Hierarchy: I have a list of type A, each instance of A has a list of type B, each instance of B has some other data including a string for a text log.
UI Structure: I have a ComboBox to select an item of type A. I have a TabControl with the tabs representing items of type B, taken from the selected A above. In each tab, there is a means to enter data to populate the object of type B, and a log, representing changes to that instance of B.
Backing Logic: I track the selected item in each list with properties (SelectionA and SelectionB in the data context, MainWindowViewModel) that notify when they change. The B object also notifies when its log text changes. These ensure that the UI responds to changes to the backing data.
Problem: I want to move the notify logic to all be in one place (the DataContext, i.e. MainWindowViewModel), rather than having some in the B class and needing to duplicate the notify logic. To achieve this, I add a property (SelectionBLogText) to track the LogText property of the SelectionB object, and bind the log (in the templated tabpanel) to the main SelectionBLogText property. The problem is that within the tabpage, I can only seem to bind to properties of the selected B object (from the selected tab), and I need to bind to a property of the DataContext instead. I've tried using RelativeSource but nothing I've tried so far works, and the more I look at the docs the more I feel it's designed for another job.
The XAML (with irrelevant details removed):
<Window x:Class="WPFQuestion.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:local="clr-namespace:WPFQuestion"
mc:Ignorable="d"
Title="MainWindow"
Height="350"
Width="930">
<DockPanel>
<ComboBox
ItemsSource="{Binding ListOfA}"
SelectedItem="{Binding SelectionA}"
DisplayMemberPath="Name"/>
<TabControl
ItemsSource="{Binding SelectionA}"
SelectedItem="{Binding SelectionB}"
DisplayMemberPath="Name">
<TabControl.ContentTemplate>
<ItemContainerTemplate>
<StackPanel>
<TextBox
IsReadOnly="True"
Text="{Binding Path=???.SelectionBLogText}"/>
<Button Click="ClearLogButton_Click"/>
</StackPanel>
</ItemContainerTemplate>
</TabControl.ContentTemplate>
</TabControl>
</DockPanel>
</Window>
And the code-behind:
public partial class MainWindow : Window
{
internal MainWindowViewModel vm;
public MainWindow()
{
InitializeComponent();
vm = new MainWindowViewModel();
DataContext = vm;
}
// Various methods for event handling
}
public class A : IEnumerable<B>
{
public string Name { get; set; }
public List<B> Bs { get; set; }
}
public class B // previously : INotifyPropertyChanged
{
public string Name { get; set; }
public string LogText { get; set; }
// various other properties
}
public class MainWindowViewModel : INotifyPropertyChanged
{
private A _a;
private B _b;
public event PropertyChangedEventHandler PropertyChanged;
public List<A> ListOfA { get; set; }
public A SelectionA
{
get => _a;
set
{
if (_a == value)
{
return;
}
_a = value;
RaisePropertyChanged(nameof(SelectionA));
}
}
public B SelectionB
{
get => _b;
set
{
if (_b == value)
{
return;
}
_b = value;
RaisePropertyChanged(nameof(SelectionB));
RaisePropertyChanged(nameof(SelectionBLogText));
}
}
public string SelectionBLogText
{
get => SelectionB.LogText;
set
{
if (SelectionB.LogText == value)
{
return;
}
SelectionB.LogText = value;
RaisePropertyChanged(nameof(SelectionBLogText));
}
}
private void RaisePropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
have you tried something like this when you used relative binding? if not please check this out.
<TextBox IsReadOnly="True"
Text="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window},
Path=Datacontext.SelectionBLogText}"/>
So I have my MainWindow.xaml which has the DataContext of BaseViewModel set like this.
<Window.DataContext>
<viewModel:BaseViewModel/>
</Window.DataContext>
it works fine, for instance when I select an item in my ListView it binds and updates my ImageView in my MainWindow.
<ListView Background="Transparent"
ItemsSource="{Binding ImageGridViewModel.Images}"
SelectedItem="{Binding ImageGridViewModel.SelectedImage}">
And then it updates like so
<Image Source="{Binding ImageGridViewModel.SelectedImage}"
Margin="20">
Perfect no issues.
However, I recently added a second view which is a Window called WatermarkWindow and I set the DataContext just like I did with the MainWindow, in the XAML like so.
<Window.DataContext>
<viewModel:BaseViewModel/>
</Window.DataContext>
And then the binding for the Image control on that new Window
<Image Source="{Binding ImageGridViewModel.SelectedImage}"
Margin="20">
However when I open that window, the Image control's source is not bound to the property, the property actually returns NULL and I think I know why that is, I think it's because in my BaseViewModel I am instantiating a new instance of that ViewModel evertime it gets called.
The reason to why I am doing it that way is because I wanted to instantiate a instance of it so I can actually use it to bind stuff. Rather than it being null.
If that's not the issue then I'm still really eager to learn and understand what the issue is.
What's the proper way of setting up a BaseViewModel that contains all the extra ViewModels?
public class BaseViewModel : ObservableObject
{
public ImageGridViewModel ImageGridViewModel { get; set; } = new ImageGridViewModel();
}
And the ObservableObject
public class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
And not that it matters because I know the properties work fine, here's the ViewModel.
public class ImageGridViewModel
{
public string ImagePath { get; set; }
public string SelectedImage { get; set; }
public ObservableCollection<string> Images { get; set; }
...
What I ended up doing was setting the DataContext property to this where I instantiate the new WawtermarkWindow
if (wmw == null)
{
wmw = new WatermarkWindow();
wmw.DataContext = this;
}
I'm trying to implement MVVM patter in my app, and got problem with data binding. Everything works pretty fine, untill a new item appears in my collection binded to ListBox. ListBox is just not updating, and after I try to click on that box exceptions about ItemsControl is thrown.
MainView.xaml
<Window.DataContext>
<ViewModel:MainViewModel/>
</Window.DataContext>
<ListBox x:Name="Scripts" ItemsSource="{Binding Scripts, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" DisplayMemberPath="Name" SelectedItem="{Binding SelectedScript}"/>
MainViewModel.cs
class MainViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public List<Script> Scripts
{
get; set;
}
public RelayCommand NewScriptCommand
{
get; set;
}
public MainViewModel()
{
Scripts = Script.Scripts;
NewScriptCommand = new RelayCommand(NewScript);
}
private void NewScript()
{
Script.NewScript();
OnPropertyChanged("Scripts");
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
And my Model, class Script.cs
public class Script
{
private static List<Script> _scripts;
public static List<Script> Scripts
{
get
{
if(_scripts == null)
{
_scripts = GetScripts();
}
return _scripts;
}
}
public static void NewScript()
{
_scripts.Add(new Script());
}
}
I was thinking about make ObservableCollection in MainViewModel, but then occurred other problem about updating that collection when List<Script> Scripts get new element.
Replace List with ObservableCollection and everything will start working. Not sure what "problem" you are talking about. Remove OnPropertyChanged("Scripts"). The Scripts property has not changed...the collection has changed. You need to raise a collection changed event which is what ObservableCollection does.
I'm having an issue with my combo box. Somehow it can get out of sync with itself. For example, after I change out my BlockSequenceFields, only the dropdown text gets altered. Below, the Field 1 has been updated but you can see that it doesn't reflect in the currently selected item.
My IsSynchronizedWithCurrentItem=true should make the currently selected item behave as expected but it doesn't seem to work. I've read many stackoverflow posts where the current item doesn't match but they just set IsSynchronizedWithCurrentItem to true and it fixes their issue.
Can anyone explain why this isn't working for me?
<ComboBox x:Name="SequenceFieldComboBox"
SelectedItem="{Binding BlockSequenceFieldIndex, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
ItemsSource="{Binding BlockSequenceFields, UpdateSourceTrigger=PropertyChanged}"
IsSynchronizedWithCurrentItem="True">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<CheckBox
IsChecked="{Binding IsCalibrated, Mode=OneWay}"
IsEnabled="False">
</CheckBox>
<TextBlock
Text="{Binding}">
</TextBlock>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
EDIT: Further details for Mr. Chamberlain
// ViewModelBase implements INotifyPropertyChanged
public class BlockFieldViewModel : ViewModelBase
{
public BlockSequenceField SequenceField { get; set; }
public List<BlockSequenceCalibrationItemViewModel> Calibrations => this.SequenceField?.CalibrationList;
public bool IsCalibrated => this.Calibrations.TrueForAll(x => x.IsCalibrated == null || x.IsCalibrated == true);
public double AmplitudeThreshold => this.Calibrations.Max(x => x.Amplitude);
public int FieldNumber { get; set; }
public override string ToString()
{
string ret = string.Format(CultureInfo.CurrentCulture, "Field {0} ", this.FieldNumber);
if (Math.Abs(this.AmplitudeThreshold) > .00001)
{
ret = string.Concat(ret, string.Format(CultureInfo.CurrentCulture, "({0} mA)", this.AmplitudeThreshold));
}
return ret;
}
}
And here is the larger viewmodel, call it MainViewModel.cs. Here are the relevant fields in the class
private ObservableCollection<BlockFieldViewModel> blockSequenceFields;
public ObservableCollection<BlockFieldViewModel> BlockSequenceFields
{
get => this.blockSequenceFields;
set
{
this.blockSequenceFields = value;
this.OnPropertyChanged("BlockSequenceFields");
}
}
private void RefreshFieldList()
{
// In order for the combo box text to update, we need to reload the items
var savedIndex = this.BlockSequenceFieldIndex; // to restore to current field.
var fieldList = this.CalibrationViewModel.FieldViewModels;
this.BlockSequenceFields = new ObservableCollection<BlockFieldViewModel>(fieldList);
this.BlockSequenceFieldIndex = savedIndex;
}
Your problem is caused because BlockFieldViewModel does not raise INPC when FieldNumber is updated. You need to raise it for that property at the minimum.
//Assuming the base class looks like
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class BlockFieldViewModel : ViewModelBase
{
//...
public int FieldNumber
{
get
{
return _fieldNumber;
}
set
{
if(_fieldNumber.Equals(value))
return;
OnPropertyChanged();
}
}
//...
}
I don't know for sure if this will solve your problem or not, due to the fact that you are using .ToString() to display the name. If you find the above does not fix it trigger a property changed for the entire object by passing a empty string in to your OnPropertyChanged method
public int FieldNumber
{
get
{
return _fieldNumber;
}
set
{
if(_fieldNumber.Equals(value))
return;
//Refresh all properties due to the .ToString() not updating.
OnPropertyChanged("");
}
}
Also, if List<BlockSequenceCalibrationItemViewModel> Calibrations can be added to or removed from, or .Amplitude could be changed you need to trigger a refresh of the name from that too.
After learning about ObservableCollection and INotifyPropertyChanged, I'm trying use them to divide my code into MVVM.
But I'm having some trouble with binding outside of code-behind class.
My app have three boxes that let you input a person's name, income, age. Then it will display them on a DataGrid.
xaml:
<Window x:Class="myApp.MainWindow"
[...]
<Grid>
<DataGrid x:Name="peopleDisplay">
</DataGrid>
</Grid>
</Window>
in MainWindow.xaml.cs (no structure)
public partial class MainWindow : Window
{
private ObservableCollection<Person> peopleList = new ObservableCollection<Person>();
public MainWindow()
{
InitializeComponent();
peopleDisplay.ItemsSource = peopleList;
}
private void btnAddProduct_Click(object sender, RoutedEventArgs e)
{
peopleList.Add(new Person { personName = nameBox.text, income = incomebox.text, age = ageBox.text });
}
[...]
}
class People : INotifyPropertyChanged
{
private string personName;
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string propName)
{
if (this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
public string PersonName {
get
{
return this.personName;
}
set
{
if( this.personName != value)
{
this.PersonName = value;
this.NotifyPropertyChanged("PersonName");
}
}
}
public int age { get; set; }
public double income { get; set; }
}
My main questions:
so now Im trying to do two things: add a new function that will calculate the total income of everyone, move the ObservableCollection above to a viewModel class
now in the new viewModel class I have the ObservableCollection personList (instead of inside behind code), but is it wrong to put the calculation method and the properties here too? If I put the calculation properties here this viewModel will be inheriting INotifyPropertyChanged, so when a the totalIncome properties changes it will change the UI automatically. it makes no sense to put it in the person model though, cause that class represent one person.
How do I bind this people List in viewModel to the xaml? If the list is in code-behind I can just do peopleDisplay.ItemsSource = peopleList;, but this viewModel is a class and not a ObservableCollection object, I cant set it to the dataGrid's ItemsSource. Is there a way to bind it in the viewModel class? Im in the progress of learning mvvm so I might be doing something wrong here too. Please advice
Your Model class is People. like below:
public class People : INotifyPropertyChanged
{
private string personName;
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public string PersonName
{
get
{
return this.personName;
}
set
{
if( this.personName != value)
{
this.PersonName = value;
this.NotifyPropertyChanged();
}
}
}
public int Age { get; set; }
public double Income { get; set; }
}
Your ViewModel like below:
public class PeopleViewModel
{
Public List<People> ListOfPeople { get; set; }
}
ViewModel can implement INotifyPropertyChanged interface to Notify the View.
Now you can set the data context as PeopleViewModel and bind your ListOfPeople to your DataGrid.
Set DataContext for your View you can do it from XAML or code behind.
Set ItemsSource for your DataGrid in your View .
XAML:
<Window x:Class="myApp.MainWindow" DataContext="{Binding PeopleViewModel }">
<Grid>
<DataGrid x:Name="peopleDisplay" ItemSource={Binding ListOfPeople}>
......
</DataGrid>
</Grid>
</Window>
Reference 1
Reference 2
1) I dont see any problem with your approach, but, what would happen if someday you want to test the method that calculate the "TotalIncome"? You could separate the calculation in an helper class.
2) First of all, you have to expose the collection in your ViewModel, using public properties. With that being said, you have to declare the binding in your xaml file.
<DataGrid x:Name="peopleDisplay"
ItemsSource="{Binding MyPropertyOnViewModel}">
</DataGrid>
Dont forget to set the DataContext of your window with your viewmodel.