I am working on Xamarin forms app and I have an instance of the ViewModel data of my current page deserialised to its correct type in my Base ViewModel. My Base ViewModel also implements INotifiyPropertyChangedInterface. The view model data that has been serialized contains the properties with the correct data.
I was expecting it to bind automatically since the INotifiyPropertyChangedInterface is implemented. But nothing happens. Do I need to do anything further? If you need any further information to help me, please ask. There was not much code that I could paste in except the deserializing the json.
Your class should look something like this:
public class Person : INotifyPropertyChanged
{
private string nameValue = string.Empty;
public event PropertyChangedEventHandler PropertyChanged;
public string Name
{
get
{
return nameValue;
}
set
{
if (value != nameValue)
{
nameValue = value;
NotifyPropertyChanged();
}
}
}
// This method should be called by the Set accessor of each property.
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Notice how it implements the INotifyPropertyChanged interface and calls the right method whenever a value is set. Failing to do so, will cause the UI not to be updated.
Now to bind your value, do this in XAML, somewhere on your page: <Label Text="{Binding Name}" />. Lastly, you have to set the BindingContext property of your page, like this:
public MyPage : ContentPage
{
public MyPage()
{
var person = new Person();
person.Name = "Bert";
BindingContext = person;
person.Name = "Ernie";
}
}
To take out the boilerplate code and call the PropertyChanged method on each property, have a look at the PropertyChanged.Fody plugin.
Related
I have a new Xamarin Forms 5 app and I'm having trouble with data binding.
First, I display a message that tells the user how many items are in his list. Initially, this is 0. It's displayed by DisplayMessage property of the view model.
Then, the Init() method gets called and once the API call is finished, there are some items in MyList. I put break points to make sure that the API call works and I end up with some data in MyList property.
Because I change the value of message in my Init() method, I was expecting the message to change and display the number of items in the list but it's not changing even though I have some items in MyList.
I created a new ViewModel that looks like this:
public class MyViewModel : BaseViewModel
{
public List<MyItem> MyList { get; set; } = new List<MyItem>();
string message = "You have no items in your list... ";
public string DisplayMessage
{
get => message;
set
{
if(message == value)
return;
message = value;
OnPropertyChanged();
}
}
public async void Init()
{
var data = await _myService.GetData();
if(data.Count > 0)
message = $"You have {data.Count} items in your list!";
MyList = data;
}
}
My MainPage code behind looks like this:
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class MainPage : ContentPage
{
MyViewModel _vm;
MainPage()
{
InitializeComponent();
_vm = new MyViewModel();
this.BindingContext = _vm;
}
protected override void OnAppearing()
{
base.OnAppearing();
_vm.Init();
}
}
I didn't change anyting in the base view model, except I added my service and it looks like this:
public class BaseViewModel : INotifyPropertyChanged
{
public IMyApiService MyApi => DependencyService.Get<IMyApiService>();
bool isBusy = false;
public bool IsBusy
{
get { return isBusy; }
set { SetProperty(ref isBusy, value); }
}
string title = string.Empty;
public string Title
{
get { return title; }
set { SetProperty(ref title, value); }
}
protected bool SetProperty<T>(ref T backingStore, T value,
[CallerMemberName] string propertyName = "",
Action onChanged = null)
{
if (EqualityComparer<T>.Default.Equals(backingStore, value))
return false;
backingStore = value;
onChanged?.Invoke();
OnPropertyChanged(propertyName);
return true;
}
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
var changed = PropertyChanged;
if (changed == null)
return;
changed.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
I'd appreciatae someone telling me where my mistake is. Thanks.
Without seeing the Xaml, I can't 100% answer, but here are a couple of things I see:
You are setting the "message" through the field, not the property. Since you are setting the field directly the OnPropertyChanged event isn't firing so the UI isn't getting notified that the value has changed.
I am guessing you are binding "MyList" to some sort of CollectionView or something? If it's a readonly view, using a List is ok as the collection is never updated. However, if you plan on adding or removing items at runtime, it needs to be an "ObservableCollection" for the same reason as above, the UI isn't notified of new items in a List, but an ObservableCollection will notify the UI of changes to it, so it can update.
Is what Jason mentions above in his comment. The MyList property should be setup like the other properties with the OnPropertyChanged.
I am using shell and I want tabs on all the pages. so I am following standard way of shell navigation but the problem is I don't know how to pass an object along with the navigation.
await Shell.Current.GoToAsync(page.ToString());
doing this for navigation
Routing.RegisterRoute("TesProject.Views.DetailView", typeof(DetailView));
Registering Route like this
I want to pass a complete object from my list view to detail view. How can I do that?
Xamarin.Forms Shell Navigation Hierarchy with parameters
I saw this but I don't think this will work in my case because I can't pass a complete model object like this.
I wrote a small example for your reference.
In the sending class, you can pass parameters like $"AferPage?param={param}".
Here is the sending code:
public partial class BeforePage : ContentPage
{
public BeforePage()
{
InitializeComponent();
}
private async void Button_Clicked(object sender, EventArgs e)
{
string param = myEntry.Text;
await Shell.Current.GoToAsync($"AferPage?param={param}");//like this
}
}
Here is the receiving class code(Implements IQueryAttributable interface):
public partial class AferPage : ContentPage, IQueryAttributable
{
public string param {get;set;}
public void ApplyQueryAttributes(IDictionary<string, string> query)
{
param = HttpUtility.UrlDecode(query["param"]);
receive.Text = param;
}
public AferPage()
{
InitializeComponent();
}
}
Using Newtonsoft.Json you can:
In List View:
var jsonStr = JsonConvert.SerializeObject([Model]);
await Shell.Current.GoToAsync($"{nameof([DetailsViewPage])}?Param={jsonStr }");
In Details View Page:
Add QueryProperty:
[QueryProperty(nameof(Param), nameof(Param))]
Convert to Model again:
var bonusesFilterData = JsonConvert.DeserializeObject<[Model]>(Param);
A solution is shown in this video: https://www.youtube.com/watch?v=YwnEpiJd0I8
You can pass objects through as a Dictionary.
For example if this is the data object you want to send:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
Set the query property on the view model of the page:
[QueryProperty(nameof(Person), nameof(Person))]
public partial class DetailViewModel
{
[ObservableProperty]
Person person;
}
Assuming your page constructor looks something like this so it will automatically set the value on your context:
public partial class DetailView : ContentPage
{
public DetailView(DetailViewModel vm)
{
InitializeComponent();
BindingContext = vm;
}
Then when you navigate to the page, pass the object in a dictionary:
Routing.RegisterRoute(nameof(DetailView), typeof(DetailView));
await Shell.Current.GoToAsync(nameof(DetailView), {
new Dictionary<string, object> {
[nameof(Person)] = person
});
Now you can access your object in bindings:
<Label Text="{Binding Person.Name}"/>
<Label Text="{Binding Person.Age}"/>
P.S. Notice the use of nameof to avoid hard coded strings.
You can user stored preferences if your json is complex like:
private async void OnItemSelected(Item item)
{
if (item == null)
return;
var jsonstr = JsonConvert.SerializeObject(item);
//Clear the shared preferences in case there is any
Preferences.Clear();
//Store your complex json on a shared preference
Preferences.Set("Data", jsonstr);
await Shell.Current.GoToAsync(nameof(DetailsPage));
}
On the details page where you retrieve your data you can have the following code:
bool hasKey = Preferences.ContainsKey("Data");
var content = Preferences.Get("Data", string.Empty);
Details details = hasKey ? JsonConvert.DeserializeObject<Model>(content) : null;
Is it possible to change viewmodel property from view? I had have tried fluent binding, but viewmodel property always null
View
public class UserProfileView : MvxActivity
{
private string _currentToken;
public string CurrentToken { get; set; }
protected override void OnCreate(Bundle bundle)
{
var accounts = AccountStore.Create(this).FindAccountsForService("Soundcloud").ToList();
var set = this.CreateBindingSet<UserProfileView, UserProfileViewModel>();
set.Bind(this).For(v => v.CurrentToken).To(vm => vm.UserToken).TwoWay();
set.Apply();
accounts.ForEach(account =>
{
CurrentToken = account.Properties["access_token"];
});
base.OnCreate(bundle);
SetContentView(Resource.Layout.UsrProfile);
}
}
ModelView
private string _userToken;
public string UserToken
{
get { return _userToken;}
set { _userToken = value; RaisePropertyChanged("UserToken"); Update();}
}
The easiest way would be inheriting from MvxActivity<TViewModel>
public class UserProfileView : MvxActivity<UserProfileViewModel>
and then simply set
ViewModel.CurrentToken = account.Properties["access_token"];
This answers your "Is it possible to change viewmodel property from view?". But this doesn't use databinding. If you really want to use data binding, you have to write a custom Binding for it, what may be too much effort in this case.
I asked can not bind textblock property from another class to UI class using MVVM in the previous post.
Can not bind textblock property from another class to UI class using MVVM
I can not still bind textblock property, but I found a new thing that PropertyChanged event becomes null when I cannot bind textblock property.
Please see bellow code (also see the previous post) :
public class Authentication : ViewModelBase
{
private string _ErrorStatus;
public string ErrorStatus
{
get
{
return _ErrorStatus;
}
set
{
_ErrorStatus = value;
NotifyPropertyChanged("ErrorStatus");
}
}
void Authenticate()
{
//The bellow code doesn't work.
ErrorStatus = "Access Denied.";
}
}
In the bellow code, PropertyChanged becomes null.
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(String info)
{
//PropertyChanged is null, so event is not called and ErrorStatus is not changed.
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
}
Please let me know how to write correct code and why PropertyChanged becomes null.
I already confirmed that ErrorStatus changes correctly when ErrorStatus is called in UI class (MainPage.cs).
I don't know if this solves your problem or not, but:
the way you are firing event is not thread-safe. Not only here but always fire your events like this:
protected void NotifyPropertyChanged(String info)
{
var handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(info));
}
}
I have a sample windows phone 7 project where I test some MVVM stuff, however I came across a problem.
My code looks like this:
This is from my View which is a MainPage:
<Grid>
<ListBox x:Name="list" ItemsSource="{Binding _reviews}"/>
</Grid>
This is code behind for the View:
public MainPage()
{
this.Loaded += MainPage_Loaded;
// Line below makes list show what it is supposed to show
// list.ItemsSource = (DataContext as MainPageVM)._reviews;
DataContext = new MainPageVM();
InitializeComponent();
}
void MainPage_Loaded(object sender, RoutedEventArgs e)
{
// DataContext is set to the right object!
var obj = list.DataContext;
}
Code for ViewModel
class MainPageVM
{
public ObservableCollection<Review> _reviews { get; set; }
public MainPageVM()
{
_reviews = GetReviews();
}
private ObservableCollection<Review> GetReviews()
{
ObservableCollection<Review> reviews = new ObservableCollection<Review>();
reviews.Add(new Review() { User = "Lol", Text = "Cool", Country = "UK"});
reviews.Add(new Review() { User = "misterX", Text = "aWESCOM APP", Country = "USA"});
reviews.Add(new Review() { User = "meYou", Text = "The best", Country = "UK"});
return reviews;
}
And here is my model:
class Review
{
public string Text { get; set; }
public string User { get; set; }
public string Country { get; set; }
}
Could you please point out where is the error and why I am able to set the ItemSource in code behind, but not via binding in XAML
The problem is that your view model class does not implement the INotifyPropertyChanged interface and you are not raising the PropertyChanged event, so the view does not know that the property you are binding to has changed.
If you're not sure about how to implement this interface, take a look at this post on Silverlight Show.
UPDATE: For most properties the above is true, however, in this instance because it's an ObservableCollection it's not necessary. However, because your view model class isn't public the view can't bind to it. Do you see any binding errors in the Output window whilst debugging?