So I am trying to implement the MVVM pattern in a simple sample app. Essentially my app allows a user to choose from a list of search providers in a SettingsPage, and then in the MainPage when the user clicks the 'search' button he or she will be navigated to the search provider's website. Everything seems to work ok, no errors, except when navigating directly back to MainPage from SettingsPage the search property does not seem to be updated. Everything is fine though when the application is completely exited and launched fresh. What I have is as follows
MainPage.xaml.cs
void search_Click(object sender, EventArgs e)
{
TheBrowser.Navigate(App.ViewModel.SearchProvider.Address);
}
App.xaml.cs
private static MainViewModel viewModel = null;
public static MainViewModel ViewModel
{
get
{
// Delay creation of the view model until necessary
if (viewModel == null)
viewModel = new MainViewModel();
return viewModel;
}
}
MainViewMode.cs
public ListItem SearchProvider { get; private set; }
public MainViewModel()
{
SearchProvider = Settings.SearchProvider.Value;
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (null != handler)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
and in my SettingsPage is where I am allowin ga user to select a search provider
SettingsPage.xaml.cs
private void PopulateSearchProviderList()
{
searchProviderList = new ObservableCollection<ListItem>();
searchProviderList.Add(new ListItem { Name = "Bing", Address = "http://www.bing.com" });
searchProviderList.Add(new ListItem { Name = "Google", Address = "http://www.google.com" });
SearchProviderListPicker.ItemsSource = searchProviderList;
}
private void stk_Tap(object sender, System.Windows.Input.GestureEventArgs e)
{
if (SearchProviderListPicker.SelectedIndex != -1)
{
var selectedItem = (sender as StackPanel).DataContext as TestApp.Classes.ListItem;
Settings.SearchProvider.Value = selectedItem; //Setting the search provider
}
}
and finally my ListItem class which is fairly straightforward
ListItem.cs
public string Name
{
get;
set;
}
public string Address
{
get;
set;
}
So essentially I am not updating the ViewModel correctly based on the SettingsPage, but I am unsure of how to go about this properly.
You have to call the OnNotifyPropertyChanged("propertyName") for the item to update in the UI.
For example (assuming the Name and Address properties are bound to your UI elements.)
private string name;
private string address;
public string Name
{
get { return name;}
set {
name = value;
OnNotifyPropertyChanged("Name");
}
}
public string Address
{
get { return address; }
set {
address = value ;
OnNotifyPropertyChanged("Address");
}
}
There are a few issues I can see. We'll start from there.
Your MainViewModel needs to implement INotifyPropertyChanged see here
Your SearchProvider setter needs to raise PropertyChanged
You need to set the value of the SearchProvider. Currently that is only performed in the constructor which is probably why you are seeing things working on app startup only.
You need to make sure you are correctly binding the value of SearchProvider in your xaml. If you post your xaml we can check that out too.
In your ViewModel, add:
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string caller = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(caller));
}
}
Update the SearchProvider property to something like:
private ListItem searchProvider;
public ListItem SearchProvider
{
get { return searchProvider; }
set
{
searchProvider = value;
OnPropertyChanged();
}
}
Related
I implemented a model class and want to raise PropertyChanged events for all subproperty when the object is modified. But I found it 's not working. When I push the button, the label's text is't changed.Does i miss something?I got this from MSDN -"The PropertyChanged event can indicate all properties on the object have changed by using either null or String.Empty as the property name in the PropertyChangedEventArgs."
the platform is .net framework 4.0 and VS2015
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
Model = new Model()
{
data = new User()
{
Name = "test"
}
};
label1.DataBindings.Add("Text", Model.data, "Name", false, DataSourceUpdateMode.OnPropertyChanged);
}
private Model model;
public Model Model
{
get
{
return this.model;
}
set
{
model = value;
}
}
private void button1_Click(object sender, EventArgs e)
{
User temp = new User()
{
Name = "test1"
};
Model.data = temp;
}
}
public class NotifyPropertyChanged : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
protected bool SetField<T>(ref T field, T value, string propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
field = value;
OnPropertyChanged(propertyName);
OnPropertyChanged(null);
return true;
}
}
public class Model : NotifyPropertyChanged
{
private User m_data;
public User data
{
get { return m_data; }
set
{
SetField(ref m_data, value,"data");
}
}
}
public class User : NotifyPropertyChanged
{
private string name;
public string Name
{
get { return this.name; }
set
{
SetField(ref name, value, "Name");
}
}
private string tel;
public string Tel
{
get { return this.tel; }
set
{
SetField(ref tel, value, "Tel");
}
}
}
Your problem is that your binding on Model.data, but later on, assign it a new value.
So the instance that is being monitored by the binding, is no more being used.
You've 2 options:
First one: don't create new User, just change it's Name:
private void button1_Click(object sender, EventArgs e)
{
Model.data.Name = "test1";
}
Or, if you really need to support both case (creation and assigment), then you have to change the binding to the Model and take the text from data.Name:
label1.DataBindings.Add("Text", Model, "data.Name", false,
DataSourceUpdateMode.OnPropertyChanged);
And the set part of the User Property in the Model to this:
set
{
SetField(ref m_data, value, "data");
this.data.PropertyChanged += (sender, args) => this.OnPropertyChanged("data");
}
So, this will create a PropertyChanged on the data, if data.Name has been changed, well if the data property itself has been set
I am having a binding issue I wasn't able to figure out for the past two days. I have thoroughly went through most of the relevant threads on SO, and I still wasn't able to pinpoint where my error lies.
The issue I'm having is with one of the textboxes in my program. The purpose of it is to show the file the user has selected from the file browser. I have bound the text property of it to a string called parameterFileSelected but the textbox never updates even though debugging seems to be showing that the iNotifyPropertyChanged is called and executed properly.
Please help me take a look at my code below if there are any mistakes in my code.
The textbox is part of an xaml called GenerateReports and this view is tied to the GenerateReportsViewModel as follows:
Code for setting datacontext to GenerateReportsViewModel
<Grid >
<Grid.DataContext>
<vm:GenerateReportsViewModel/>
</Grid.DataContext>
<Grid.ColumnDefinitions>
....
Code for TextBox. I have tried removing the Twoway mode, changing it to Oneway and removing the mode but there is no difference.
<TextBox Grid.Column="2" Grid.Row="1" Margin="5" Text="{Binding parameterFileSelected, Mode=Twoway, UpdateSourceTrigger=PropertyChanged}" ></TextBox>
To get the file browser and then to pass the selected file result to the GenerateReportsViewModel, this is the function in the codebehind file. The genviewmodel is initialized in the beginning of the codebehind file as GenerateReportsViewModel genViewModel = new GenerateReportsViewModel();
private void ParaFileButtonClick(object sender, RoutedEventArgs e)
{
OpenFileDialog openFileDialog = new OpenFileDialog();
if (openFileDialog.ShowDialog() == true)
{
DataContext = genViewModel;
genViewModel.updateParameterFileSelected(openFileDialog.FileName.ToString());
}
}
This is the code that's called in GenerateReportsViewModel to update the parameterFileSelected string the textbox is bound to.
class GenerateReportsViewModel : ViewModelBase
{
private string _parameterFileSelected;
public string parameterFileSelected
{
get { return _parameterFileSelected; }
set { SetValue(ref _parameterFileSelected, value); }
}
public void updateParameterFileSelected(string parameterFile)
{
parameterFileSelected = parameterFile;
}
}
Here is the ViewModelBase the viewmodel is attached to.
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void SetValue<T>(ref T property, T value, [CallerMemberName] string propertyName = null)
{
if (property != null)
{
if (property.Equals(value)) return;
}
OnPropertyChanged(propertyName);
property = value;
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
var handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
EDIT
Working Solution after Applying Kevin's Suggestions
For simplicity sake, the Datacontext was set in the XAML.
<Grid>
<Grid.DataContext>
<vm:GenerateReportsViewModel x:Name="generateReportsViewModel"/>
</Grid.DataContext>
Then, I call the string the textbox was bound to, in the viewmodel directly from code behind.
private void ParaFileButtonClick(object sender, RoutedEventArgs e)
{
OpenFileDialog openFileDialog = new OpenFileDialog();
if (openFileDialog.ShowDialog() == true)
{
generateReportsViewModel.parameterFileSelected = openFileDialog.FileName.ToString();
}
}
The ViewModel now uses Kevin's ViewModelBase:
public class GenerateReportsViewModel : ViewModelBase
{
public string parameterFileSelected
{
get { return this.GetValue<string>(); }
set { this.SetValue(value); }
}
}
Thank you Kevin for your solution. Now my 2-day-long problem is solved.
I found out that my previous ViewModelBase was calling iNotifyPropertyChanged but somehow when the View was updated, the value was null instead.
I'm trying to understand why using the ref keyword in your viewModel. I learned a nice way to create the BaseViewModel from the Classon and Baxter book which you can find below. The view-model implements the INotifyPropertyChanged like you did. What you did with [CallerMemberName] is great, it's really magical the way we can reference to our properties thanks to it.
The view model uses a the dictionary to store its properties. It uses a pretty neat trick of looking through the dictionnary keys to see if we contain the string name of the property.Otherwise, we will return a default T value.
public class CommonBaseViewModel: INotifyPropertyChanged
{
private Dictionary<string, object> Values { get; set; }
protected CommonBaseViewModel()
{
this.Values = new Dictionary<string, object>();
}
public event PropertyChangedEventHandler PropertyChanged;
protected T GetValue<T>([CallerMemberName] string name=null)
{
if (this.Values.ContainsKey(name))
{
return (T)this.Values[name];
}
else
{
return default(T);
}
}
protected void SetValue(object value, [CallerMemberName] string name = null)
{
this.Values[name] = value;
//notify my property
this.OnPropertyChanged(new PropertyChangedEventArgs(name));
}
protected void OnPropertyChanged([CallerMemberName] string name=null)
{
this.OnPropertyChanged(new PropertyChangedEventArgs(name));
}
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
{
if(this.PropertyChanged != null)
{
this.PropertyChanged(this, e);
}
}
}
As for your GenerateReportViewModel, with the common view model that I provided you, your class then becomes :
public class GenerateReportsViewModel : CommonViewModelBase
{
private string _parameterFileSelected;
public string parameterFileSelected
{
get { return _parameterFileSelected; }
set { SetValue(ref _parameterFileSelected, value); }
}
get
{
return this.GetValue<string>();
}
set
{
this.SetValue(value);
}
public void updateParameterFileSelected(string parameterFile)
{
parameterFileSelected = parameterFile;
}
}
Oh before I forgot, I don't know if it was your intention, but your GenerateReportViewModel is private. This has some impact on your code. Don't forget that by defaut, classes are private!
As for your code behind, even though it could be consider bad practice, I recommend that you have a private field (OpenFileDialog _openFileDialog)that you construct while initializing your page. Because doing it each time your clicking your button is going to consume more data that you need your application to.
//EDIT
I have review my code,and it seemed that the property was not programmed correctly.
public class GenerateReportsViewModel : CommonViewModelBase
{
private string _parameterFileSelected;
public string parameterFileSelected
{
get
{
return this.GetValue<string>();
}
set
{
this.SetValue(value);
}
public void updateParameterFileSelected(string parameterFile)
{
parameterFileSelected = parameterFile;
}
}
More about my comment about constructing the page and binding the view model. While creating your page, you have to create the view-model for that page and then bind it to the data context.
I don't know what you do in your code, but I could provide with this sample such as
public GenerateReportView()
{
InitializeComponent();
//Some operations
var generateReportViewModel = new GenerateReportViewModel();
this.DataContext = generateReportViewModel;
}
I don't understand why when I update a object, my bound controls do not update.
The data displays fine initially, but when I want to refresh the data displayed in the UI nothing happens when I update the object. The object updates fine. The ViewModel does use INotifyPropertyChanged on all fields.
However if I update individual items directly, I can update my UI. As commented below.
I guess I've made a school boy error somewhere here?
UPDATE: I've added the model to the question. While I understand the answers, I don't understand how to implement it. Attempted to implement a collection changed event without success. Can I have some pointers please?
public partial class CisArrivalsPanel : UserControl
{
private ApiDataArrivalsDepartures _theArrivalsDepartures;
public CisArrivalsPanel()
{
InitializeComponent();
_theArrivalsDepartures = new ApiDataArrivalsDepartures();
_theArrivalsDepartures = MakeQuery.LiveTrainArrivals("London Kings Cross");
this.DataContext = _theArrivalsDepartures;
ListBoxArr.ItemsSource = _theArrivalsDepartures.StationMovementList;
}
void Reload()
{
//This does not update the UI**
_theArrivalsDepartures = MakeQuery.LiveTrainArrivals("London Paddington");
//However this (when uncommented, and I comment out the above line) does update the UI**
//_theArrivalsDepartures.StationMovementList[0].OriginName = "test";
//_theArrivalsDepartures.StationMovementList[0].Platform = "0";
//_theArrivalsDepartures.StationMovementList[0].BestArrivalEstimateMins = "999";
//_theArrivalsDepartures.StationName = "test";
}
private void StationHeader_OnPreviewMouseDown(object sender, MouseButtonEventArgs e)
{
Reload();
Debug.WriteLine(_theArrivalsDepartures.StationName);
foreach (var a in _theArrivalsDepartures.StationMovementList)
{
Debug.WriteLine(a.OriginName);
Debug.WriteLine(a.BestArrivalEstimateMins);
}
}
}
EDIT : Added Model
public class ApiDataArrivalsDepartures : INotifyPropertyChanged
{
private string _stationName;
[JsonProperty(PropertyName = "station_name")]
public string StationName {
get
{
return _stationName;
}
set
{
_stationName = value;
NotifyPropertyChanged("StationName");
}
}
private List<StationListOfMovements> _stationMovementList;
public List<StationListOfMovements> StationMovementList
{
get
{
return _stationMovementList;
}
set
{
_stationMovementList = value;
NotifyPropertyChanged("StationMovementList");
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(string property)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
}
public class StationListOfMovements : INotifyPropertyChanged
{
private string _originName;
[JsonProperty(PropertyName = "origin_name")]
public string OriginName {
get
{
return _originName;
}
set
{
_originName = value;
NotifyPropertyChanged("OriginName");
}
}
[JsonProperty(PropertyName = "destination_name")]
public string DestinationName { get; set; }
private string _platform;
[JsonProperty(PropertyName = "Platform")]
public string Platform {
get
{
return _platform;
}
set
{
_platform = value;
NotifyPropertyChanged("Platform");
}
}
private string _bestArrivalEstimateMins;
[JsonProperty(PropertyName = "best_arrival_estimate_mins")]
public string BestArrivalEstimateMins {
get
{
return _bestArrivalEstimateMins;
}
set
{
_bestArrivalEstimateMins = value;
NotifyPropertyChanged("BestArrivalEstimateMins");
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(string property)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
}
There are two pieces here pertaining to your collection (technically three):
If you want a new collection to propagate, the collection property has to raise PropertyChanged (sounds like it does)
If you want add/remove on the collection to propagate, you need to use a collection that implements INotifyCollectionChanged. ObservableCollection is a good choice.
If you want changes to the items in the container to propagate, then those items need to implement INotifyPropertyChanged and raise the PropertyChanged event.
Make sure all those are covered, and the changes should appear on the UI as you expect.
You should update the DataContext and ItemsSource too.
void Reload()
{
//This does not update the UI**
_theArrivalsDepartures = MakeQuery.LiveTrainArrivals("London Paddington");
DataContext = theArrivalsDepartures;
ListBoxArr.ItemsSource = _theArrivalsDepartures.StationMovementList;
}
Use for the collection ObservableCollection , this class notify the ui when change to the collection occurred
your reload function works because the there is PropertyChanged on all the fields include this one
it notify the ui and reload the correct collection
I have a page that already has a DataContext.
When i change the pivot item, I need to bind another list to another collection.
How to achieve this?
Here is the first DataContext that shows first pivotitem info.
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
if (NavigationContext.QueryString.TryGetValue("id", out _embarqueId))
{
String json = JsonConvert.SerializeObject(_embarqueId);
using (IntrepService service = new IntrepService())
{
String retornojson = service.ObterDetalhesEmbarque(json);
EmbarqueAtual = JsonConvert.DeserializeObject<EmbarqueViewModel>(retornojson);
DataContext = EmbarqueAtual;
}
VerificaConclusao();
}
}
Then I try to load the second collection to the listbox, but doesn't work:
private void Pivot_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (!_itemsareloaded && ((PivotItem)pivot.SelectedItem).Header.Equals("itens"))
{
using (IntrepService service = new IntrepService())
{
String json = JsonConvert.SerializeObject(_embarqueId);
var retorno = service.ObterItensEmbarque(json);
ItensDoEmbarque = JsonConvert.DeserializeObject<ObservableCollection<ItemDeEmbarqueViewModel>>(retorno);
lstItens.DataContext = ItensDoEmbarque;
}
}
}
You should have one ViewModel to hold all of your data that you want to bind to. Set this ViewModel as your datacontext.
public class ViewModel : INotifyPropertyChanged
{
private ObservableCollection<ItemDeEmbarqueViewModel> _itensDoEmbarque;
private EmbarqueViewModel _embarqueAtual;
public ViewModel()
{
ItensDoEmbarque = new ObservableCollection<ItemDeEmbarqueViewModel>();
}
public event PropertyChangedEventHandler PropertyChanged;
public ObservableCollection<ItemDeEmbarqueViewModel> ItensDoEmbarque
{
get { return _itensDoEmbarque; }
set
{
_itensDoEmbarque= value;
OnPropertyChanged("ItensDoEmbarque");
}
}
public EmbarqueViewModel EmbarqueAtual
{
get { return _embarqueAtual; }
set
{
_embarqueAtual = value;
OnPropertyChanged("EmbarqueAtual");
}
}
protected virtual void OnPropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Within your OnNavigatedTo method set both of the properties and set the DataContext to be this object. You xaml would need to change to bind to the properties of these items instead of {Binding}
You can set the collections that the PivotItem will be bound to ahead of time without worry of rendering delay. PivotItems delay rendering until they are shown.
I have a listbox which is databound to a collection of objects.
I want to modify the way the items are displayed to show the user which one of these objects is the START object in my program.
I tried to do this the following way, but the listbox does not automatically update.
Invalidating the control also didn't work.
The only way I can find is to completely remove the databindings and add it back again. but in my case that is not desirable.
Is there another way?
class Person : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _name;
public string Name
{
get
{
if (PersonManager.Instance.StartPerson == this)
return _name + " (Start)";
return _name;
}
set
{
_name = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("Name"));
}
}
public Person(string name)
{
Name = name;
}
}
This is the class wich manages the list and the item that is the start
class PersonManager
{
public BindingList<Person> persons { get; set; }
public Person StartPerson { get; set; }
private static PersonManager _instance;
public static PersonManager Instance
{
get
{
if (_instance == null)
{
_instance = new PersonManager();
}
return _instance;
}
}
private PersonManager()
{
persons = new BindingList<Person>();
}
}
In the form I use the following code
private void button1_Click(object sender, EventArgs e)
{
PersonManager.Instance.StartPerson = (Person)listBox1.SelectedItem;
}
I'm pretty sure that the problem is that, when you do this, you're effectively making the Person.Name properties "get" accessor change the value (and act like a set accessor as far as the UI is concerned).
However, there is nothing that's updating the bindings to say that this is happening. If PropertyChanged got called when you set start, I believe this would update.
It's clunky, but the way you have it written, I believe you could add this and make it work (NOTE: I didn't test this, so it ~may~ have issues):
private void button1_Click(object sender, EventArgs e)
{
Person newStart = (Person)listBox1.SelectedItem;
if (newStart != null)
{
PersonManager.Instance.StartPerson = newStart;
newStart.Name = newStart.Name; // Dumb, but forces a PropertyChanged event so the binding updates
}
}