MvvmCross bind Viewmodel property to view property - xamarin

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.

Related

DataBinding issue in Xamarin Forms

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.

Xamarin.forms- pass object while navigation with Shell

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;

Update Properties from Base ViewModel

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.

Custom cell is not updating viewmodel when user enters input into cell

I am new into MVVMCross(Xamarin.iOS). So, I could be in wrong direction. If somebody can point me in right direction or point out what I am doing wrong.
I have already taken a look over "CustomerManagement" and "Collection" sample of MVVMCross.
Basically I am trying to create Settings screen.
I have one custom UITableViewCell with one text field. See my code below. "EntryTF" is IBOutleted. "EntryTF" is binded with "FirstName" property of Model. Setting value to "FirstName" is reflecting on UI. But if user enter something to textfield doesn't save to model. In short cell is not updating viewmodel/model.
Please note that I want to keep binding out of cell class. So, I can reuse this cell for other models or fields.
public partial class PlainEntryCell : UITableViewCell
{
public static readonly UINib Nib = UINib.FromName ("PlainEntryCell", NSBundle.MainBundle);
public static readonly NSString Key = new NSString ("PlainEntryCell");
public PlainEntryCell (IntPtr handle) : base (handle)
{
// this.DelayBind (() => {
// this.AddBindings(new Dictionary<object,string> ())
// });
}
public static PlainEntryCell Create ()
{
return (PlainEntryCell)Nib.Instantiate (null, null) [0];
}
public string CaptionText {
get {
return EntryTF.Text;
}
set {
EntryTF.Text = value;
}
}
}
My View Model:
public class RegisterViewModel: MvxViewModel
{
private RegisterModel _registerModel;
public RegisterViewModel ()
{
_registerModel = new RegisterModel ();
_registerModel.FirstName = "Test";
}
public RegisterModel Customer {
get { return _registerModel; }
set {
_registerModel = value;
RaisePropertyChanged ("Customer");
}
}
}
Model:
public class RegisterModel:MvxNotifyPropertyChanged
{
private string _firstName;
public string FirstName {
get { return _firstName; }
set {
_firstName = value;
RaisePropertyChanged ("FirstName");
}
}
public string LastName { get; set; }
public string PhoneNum { get; set; }
public string Email { get; set; }
public string Pin { get; set; }
}
TableView Source:
public class RegisterTableViewSource: MvxTableViewSource
{
RegisterView _registerView;
public RegisterTableViewSource (UITableView tableView, RegisterView registerView)
: base (tableView)
{
_registerView = registerView;
tableView.RegisterNibForCellReuse (PlainEntryCell.Nib,
PlainEntryCell.Key);
//tableView.RegisterNibForCellReuse (UINib.FromName ("DogCell", NSBundle.MainBundle), DogCellIdentifier);
}
protected override UITableViewCell GetOrCreateCellFor (UITableView tableView, Foundation.NSIndexPath indexPath, object item)
{
var cell = TableView.DequeueReusableCell (PlainEntryCell.Key, indexPath);
cell.Bind (_registerView, "CaptionText Customer.FirstName");
return cell;
//return (UITableViewCell)TableView.DequeueReusableCell (PlainEntryCell.Key, indexPath);
}
public override nint RowsInSection (UITableView tableview, nint section)
{
return 2;
}
}
Update:
Still not able to get the answer.
In above code I want to bind "EntryTF" with property of model. But I want to keep binding outside class. So, CaptionText property is not necessary if someone can point out direct way of binding to "EntryTF"
Is not there a way of creating BindableProperty just like Xamarin Forms? I feel MVVMCross is matured framework so, why there is not a solution of this kind of simple things.
I would also love to here if there is any simple/other way of achieving the same things.
I have also looked MTD but didn't find much useful for custom cell and it is also need good amount of my learning.
Basically I am trying to create Settings screen.
Take a look at using monotouch dialog, mt.d, with MvvmCross, Use Mvvmcross Binding with MonoTouch.Dialog (Lists and Commands)
That StackOverflow question/answer will get you started using mt.d with mvvmcross. I use it for basic settings screens in my applications.
Cell Isn't Updating
I wrote an article on using custom cells with MvvmCross, see
http://benjaminhysell.com/archive/2014/04/mvvmcross-custom-mvxtableviewcell-without-a-nib-file/
I have found it easier not to use nib files and fully describe my UI in code.
It looks like in your public partial class PlainEntryCell : UITableViewCell you don't ever bind the cell to the ViewModel. You have that commented out. I would try something like, while adding http://slodge.blogspot.com/2013/07/playing-with-constraints.html to your application for layout:
public PlainEntryCell()
{
CreateLayout();
InitializeBindings();
}
UILabel captionText;
private void CreateLayout()
{
SelectionStyle = UITableViewCellSelectionStyle.None;
Accessory = UITableViewCellAccessory.DisclosureIndicator;
captionText = new UILabel();
ContentView.AddSubviews(captionText);
ContentView.SubviewsDoNotTranslateAutoresizingMaskIntoConstraints();
const int vPadding = 10;
const int hPadding = 20;
ContentView.AddConstraints(
captionText.AtTopOf(ContentView).Plus(vPadding),
captionText.AtLeftOf(ContentView).Plus(hPadding),
captionText.Width().EqualTo(UIScreen.MainScreen.Bounds.Width / 2)
);
private void InitializeBindings()
{
this.DelayBind(() =>
{
var set = this.CreateBindingSet<PlainEntryCell, RegisterViewModel();
set.Bind(captionText).To(vm => vm.CaptionText);
set.Apply();
});
}
}
Your custom cell needs to implement INotifyPropertyChanged to allow the ViewModel to be notified of a value change in your cell properties.
public partial class PlainEntryCell : UITableViewCell, INotifyPropertyChanged
{
public static readonly UINib Nib = UINib.FromName ("PlainEntryCell", NSBundle.MainBundle);
public static readonly NSString Key = new NSString ("PlainEntryCell");
public PlainEntryCell (IntPtr handle) : base (handle)
{
// this.DelayBind (() => {
// this.AddBindings(new Dictionary<object,string> ())
// });
}
public static PlainEntryCell Create ()
{
return (PlainEntryCell)Nib.Instantiate (null, null) [0];
}
public string CaptionText {
get {
return EntryTF.Text;
}
set {
EntryTF.Text = value;
RaisePropertyChanged("CaptionText");
}
}
#region INotifyPropertyChanged implementation
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void RaisePropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}

MVC3 Validation with Lightspeed

My ORM (LightSpeed) generates this for Animals table, with Name and Age. Using MVC3 and Razor
[Serializable]
[System.CodeDom.Compiler.GeneratedCode("LightSpeedModelGenerator", "1.0.0.0")]
[System.ComponentModel.DataObject]
[Table(IdColumnName="AnimalID", IdentityMethod=IdentityMethod.IdentityColumn)]
public partial class Animal : Entity<int>
{
[ValidatePresence]
[ValidateLength(0, 50)]
private string _name;
[ValidateComparison(ComparisonOperator.GreaterThan, 0)]
private int _age;
public const string NameField = "Name";
public const string AgeField = "Age";
[System.Diagnostics.DebuggerNonUserCode]
[Required] // ****I put this in manually to get Name required working
public string Name
{
get { return Get(ref _name, "Name"); }
set { Set(ref _name, value, "Name"); }
}
[System.Diagnostics.DebuggerNonUserCode]
public int Age
{
get { return Get(ref _age, "Age"); }
set { Set(ref _age, value, "Age"); }
}
With [Required] attribute added:
With no [Required] attribute added: (notice LightSpeed strange rendering of validation)
With name filled in:
In images above - the validation at the top is LightSpeed (put into ValidationSummary) and at the side is MVC3 (put into ValidationMessageFor)
Am only using Server Side validation currently.
Question: How do I get LightSpeed validation working well in MVC3?
I think it is something in this area http://www.mindscapehq.com/staff/jeremy/index.php/2009/03/aspnet-mvc-part4/
For the server side validation - you will want to use a custom model binder which emits the errors from LightSpeed validation more precisely rather than the leveraging the DefaultModelBinder behavior. Have a look at either directly using or adapting the EntityModelBinder from the community code library for Mvc
http://www.mindscapehq.com/forums/Thread.aspx?PostID=12051
See link http://www.mindscapehq.com/forums/Thread.aspx?ThreadID=4093
Jeremys answer (Mindscape have great support!)
public class EntityModelBinder2 : DefaultModelBinder
{
public static void Register(Assembly assembly)
{
ModelBinders.Binders.Add(typeof(Entity), new EntityModelBinder2());
foreach (Type type in assembly.GetTypes())
{
if (typeof(Entity).IsAssignableFrom(type))
{
ModelBinders.Binders.Add(type, new EntityModelBinder2());
}
}
}
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
object result = base.BindModel(controllerContext, bindingContext);
if (typeof(Entity).IsAssignableFrom(bindingContext.ModelType))
{
Entity entity = (Entity)result;
if (!entity.IsValid)
{
foreach (var state in bindingContext.ModelState.Where(s => s.Value.Errors.Count > 0))
{
state.Value.Errors.Clear();
}
foreach (var error in entity.Errors)
{
if (error.ErrorMessage.EndsWith("is invalid")) continue;
bindingContext.ModelState.AddModelError(error.PropertyName ?? "Custom", error.ErrorMessage);
}
}
}
return result;
}
}
and in Global.asax register using:
EntityModelBinder2.Register(typeof(MyEntity).Assembly);
The Register call sets up the model binder to be used for each entity type in your model assembly so modify as required.
You can get client side validation working with Lightspeed nightly builds from 04/04/2011 onwards.
Create a validator provider as follows:
public class LightspeedModelValidatorProvider : DataAnnotationsModelValidatorProvider
{
private string GetDisplayName(string name)
{
return name; // go whatever processing is required, eg decamelise, replace "_" with " " etc
}
protected override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context, IEnumerable<Attribute> attributes)
{
if(typeof(Entity).IsAssignableFrom(metadata.ContainerType))
{
List<Attribute> newAttributes = new List<Attribute>(attributes);
var attr = DataAnnotationBuilder.GetDataAnnotations(metadata.ContainerType, metadata.PropertyName, GetDisplayName(metadata.PropertyName));
newAttributes.AddRange(attr);
return base.GetValidators(metadata, context, newAttributes);
}
return base.GetValidators(metadata, context, attributes);
}
}
Then in Application_Start() add
ModelValidatorProviders.Providers.Clear();
ModelValidatorProviders.Providers.Add(new LightspeedModelValidatorProvider());

Resources