I was stuck in this exception when starting using Xamarin forms with MVVM implementation
Method not found: 'Xamarin.Forms.BindableObjectExtensions.SetBinding'.
It failed at the line var mainNav = new MainPage ()
public static Page GetMainPage ()
{
RegisterTypes ();
var mainNav = new MainPage ();
return mainNav;
}
Here is my code, I have remove the unrelated codes to keep it simple. As you can see, it is very basic, and I knew I must get something very basic wrong, but just can't figure it out.
Thanks in advance....
View
public class MainPage :ContentPage
{
public MainPage ()
{
BindingContext = new MainPageViewModel ();
var nameEntry = new Entry ();
nameEntry.SetBinding (Entry.TextProperty, "Name");
Content = new StackLayout
{
Spacing = 12,
Padding = 20,
VerticalOptions = LayoutOptions.Start,
Children = { nameEntry }
};
}
}
ViewModel
public class MainPageViewModel:BaseViewModel
{
private string _name;
public string Name
{
get
{
return _name;
}
set
{
_name = value;
OnPropertyChanged();
}
}
}
I tried your exact code in a new project (Xamarin 3.9) and it worked fine (I only tested Android).
I omitted the RegisterTypes() from GetMainPage() and implemented the BaseViewModel as follows:
public class BaseViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
Maybe you just need to update your Xamarin?
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 trying to get some strings into current.properties but am not sure how to do so the right way.
Right now i am creating a Label with the binding and then afterwards getting the label.text into the property but it seems to a bit stupid:
var userLabel = new Label {};
userLabel.SetBinding(Label.TextProperty, "name");
Application.Current.Properties["studentName"] = userLabel.Text;
Looking forward to an easier way ;-)
Here is a workaround:
class MyViewModel : INotifyPropertyChanged
{
string text;
public event PropertyChangedEventHandler PropertyChanged;
public MyViewModel()
{
Text = "York";
}
public string Text
{
set
{
if (text != value)
{
text = value;
OnPropertyChanged("Text");
Application.Current.Properties["Text"] = text;
}
}
get
{
return text;
}
}
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Set BindingContext:
protected override void OnAppearing()
{
base.OnAppearing();
MyViewModel mainPageViewModel = new MyViewModel();
BindingContext = mainPageViewModel;
}
Use it in your label:
<Label Text="{Binding Text}">
For more detailed information, you could refer to: Xamarin Forms Data Binding Basics.
In my ContentPage I subscribe with the MessageCenter waiting on an event to occur. When I receive that message I need to update my ViewModel that has a BindingContext to my ContentPage, like so:
Page
public class MyPage : ContentPage
{
public MyPage()
{
Model = new ViewModel();
MessagingCenter.Subscribe<Application>(Application.Current, "MyMessage", (sender) =>
{
Model.Activated = true;
});
// ...
Title = "My Page";
Content = stackLayout;
BindingContext = Model;
}
public ViewModel Model { get; private set; }
}
View Model
public class ViewModel : INotifyPropertyChanged
{
private bool _activated;
public event PropertyChangedEventHandler PropertyChanged;
public bool Activated
{
get { return _activated; }
set
{
_activated = value;
PropertyChanged(this, new PropertyChangedEventArgs(nameof(Activated)));
}
}
}
Whenever I try to set Model.Activated = true; from the message subscription, I get a null reference exception on the PropertyChangedEventHandler (PropertyChanged) in my ViewModel here:
PropertyChanged(this, new PropertyChangedEventArgs(nameof(Activated)));
I assume this is because the message center is running on a background thread or something.
How do I fix this?
Move the bindingcontext assignment before Messagecenter function
Does Caliburn.Micro 3.0 (and Caliburn.Micro.Xamarin.Forms) implement functionality to mimic/support Navigation.PushModalAsync in Xamarin.Forms?
No. It's not build in, but its easy to enhance it. Usually, MvvM frameworks are navigating by ViewModels. Caliburn is following this pattern. So it needs some kind of navigation service. This navigationservice is responsible for creating the Views for the ViewModels and call the view framework (Xamarin.Froms in our case) specific navigation functions. NavigationPageAdapter is the thing we are searching for. Now let's enhance it.
public interface IModalNavigationService : INavigationService
{
Task NavigateModalToViewModelAsync<TViewModel>(object parameter = null, bool animated = true);
// TODO: add more functions for closing
}
public class ModalNavigationPageAdapter : NavigationPageAdapter, IModalNavigationService
{
private readonly NavigationPage _navigationPage;
public ModalNavigationPageAdapter(NavigationPage navigationPage) : base(navigationPage)
{
_navigationPage = navigationPage;
}
public async Task NavigateModalToViewModelAsync<TViewModel>(object parameter = null, bool animated = true)
{
var view = ViewLocator.LocateForModelType(typeof(TViewModel), null, null);
await PushModalAsync(view, parameter, animated);
}
private Task PushModalAsync(Element view, object parameter, bool animated)
{
var page = view as Page;
if (page == null)
throw new NotSupportedException(String.Format("{0} does not inherit from {1}.", view.GetType(), typeof(Page)));
var viewModel = ViewModelLocator.LocateForView(view);
if (viewModel != null)
{
TryInjectParameters(viewModel, parameter);
ViewModelBinder.Bind(viewModel, view, null);
}
page.Appearing += (s, e) => ActivateView(page);
page.Disappearing += (s, e) => DeactivateView(page);
return _navigationPage.Navigation.PushModalAsync(page, animated);
}
private static void DeactivateView(BindableObject view)
{
if (view == null)
return;
var deactivate = view.BindingContext as IDeactivate;
if (deactivate != null)
{
deactivate.Deactivate(false);
}
}
private static void ActivateView(BindableObject view)
{
if (view == null)
return;
var activator = view.BindingContext as IActivate;
if (activator != null)
{
activator.Activate();
}
}
}
We just declared the interface IModalNavigationService that extends INavigationService and implement it in our ModalNavigationPageAdapter. Unfortunately Caliburn made alot of functions private, so we have to copy them over to our inherited version.
In caliburn you can navigate via navigationservice.For<VM>().Navigate(). We want to follow this style, so we have to implement something like navigationservice.ModalFor<VM>().Navigate() which we do in an extension method.
public static class ModalNavigationExtensions
{
public static ModalNavigateHelper<TViewModel> ModalFor<TViewModel>(this IModalNavigationService navigationService)
{
return new ModalNavigateHelper<TViewModel>().AttachTo(navigationService);
}
}
This method returns a ModalNavigateHelperthat simplifies the usage of our navigation service (similar to Caliburn's NavigateHelper). It's nearly a copy, but for the IModalNavigationService.
public class ModalNavigateHelper<TViewModel>
{
readonly Dictionary<string, object> parameters = new Dictionary<string, object>();
IModalNavigationService navigationService;
public ModalNavigateHelper<TViewModel> WithParam<TValue>(Expression<Func<TViewModel, TValue>> property, TValue value)
{
if (value is ValueType || !ReferenceEquals(null, value))
{
parameters[property.GetMemberInfo().Name] = value;
}
return this;
}
public ModalNavigateHelper<TViewModel> AttachTo(IModalNavigationService navigationService)
{
this.navigationService = navigationService;
return this;
}
public void Navigate(bool animated = true)
{
if (navigationService == null)
{
throw new InvalidOperationException("Cannot navigate without attaching an INavigationService. Call AttachTo first.");
}
navigationService.NavigateModalToViewModelAsync<TViewModel>(parameters, animated);
}
}
Last but not least, we have to use our shiny new navigation service instead of the old one. The App class is registering the NavigationPageAdapter for the INavigationService as singleton in PrepareViewFirst. We have to change it as follows
public class App : FormsApplication
{
private readonly SimpleContainer container;
public App(SimpleContainer container)
{
this.container = container;
container
.PerRequest<LoginViewModel>()
.PerRequest<FeaturesViewModel>();
Initialize();
DisplayRootView<LoginView>();
}
protected override void PrepareViewFirst(NavigationPage navigationPage)
{
var navigationService = new ModalNavigationPageAdapter(navigationPage);
container.Instance<INavigationService>(navigationService);
container.Instance<IModalNavigationService>(navigationService);
}
}
We are registering our navigation service for INavigationService and IModalNavigationService.
As you can see in the comment, you have to implement close functions that call PopModalAsync by yourself.
I am getting wrong position in custom ListView while scrolling.
I have tried the ViewHolder pattern and an ArrayAdapter but both giving the same problem.
If I reproduce the code using Java then I am getting the proper position while scrolling.
So is it Xamarin architecture bug ?
Below is my sample code:
Activity Class
namespace ArrayAdapterDemoApp
{
[Activity(Label = "ArrayAdapterDemoApp", MainLauncher = true,
Icon ="#drawable/icon")]
public class MainActivity : Activity
{
private static List<DataBean> _ItemsList = new List<DataBean>();
private static CustomAdapter _adapter;
private ListView _listview;
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
// Set our view from the "main" layout resource
SetContentView(Resource.Layout.Main);
// Get our button from the layout resource,
// and attach an event to it
_listview = FindViewById<ListView>(Resource.Id.mylist);
DataBean obj1 = new DataBean();
obj1.Name = "AA";
obj1.City = "11";
_ItemsList.Add(obj1);
DataBean obj2 = new DataBean();
obj2.Name = "BB";
obj2.City = "22";
_ItemsList.Add(obj2);
DataBean obj3 = new DataBean();
obj3.Name = "CC";
obj3.City = "33";
_ItemsList.Add(obj3);
...
DataBean obj15 = new DataBean();
obj15.Name = "OO";
obj15.City = "1010";
_ItemsList.Add(obj15);
_adapter = new CustomAdapter(this, _ItemsList);
_listview.Adapter = _adapter;
}
}
}
Custom Adapter
namespace ArrayAdapterDemoApp
{
public class CustomAdapter : ArrayAdapter<DataBean>
{
private class TaskViewHolder : Java.Lang.Object
{
public TextView tvName;
public TextView tvCity;
}
List<DataBean> listData;
Activity _context;
int _position;
public CustomAdapter(Activity context, List<DataBean> dataList)
: base(context, Resource.Layout.adapter_row, dataList)
{
this._context = context;
this.listData = dataList;
}
public override long GetItemId(int position)
{
return position;
}
public override int Count
{
get { return listData.Count; }
}
//With View Holder
public override View GetView(int position, View convertView, ViewGroup parent)
{
DataBean data = listData[position];
TaskViewHolder viewHolder= null; // view lookup cache stored in tag
if (convertView == null)
{
viewHolder = new TaskViewHolder();
LayoutInflater inflater = LayoutInflater.From(_context);
convertView = inflater.Inflate(Resource.Layout.adapter_row, parent, false);
viewHolder.tvName = convertView.FindViewById<TextView>(Resource.Id.text1);
viewHolder.tvCity = convertView.FindViewById<TextView>(Resource.Id.text2);
convertView.Tag = viewHolder;
}
if(viewHolder==null)
{
viewHolder = (TaskViewHolder)convertView.Tag;
}
viewHolder.tvName.Text = data.Name;
viewHolder.tvCity.Text = data.City;
return convertView;
}
}
}
DataBean Class
namespace ArrayAdapterDemoApp
{
public class DataBean
{
public string Name { get; set; }
public string City { get; set; }
}
}
I had also same issue so I resolved it by just Tag the position to the view.for example.
//iv_delete is a imageview
holder.iv_delete.Tag = position;
and get the position from the Tag
For me it was like that
int finalPosition = (Integer)holder.iv_delete.Tag.IntValue();
Enjoy!!!!!
Add in your adapter class this method:
public DataBean GetItemAtPosition(int position)
{
return this.listData[position];
}
You can tag the position of each row to the control to get the correct position or
Another method is by using action event binding to each view row. This also solves duplicate method call issue.
this might be helpful: http://appliedcodelog.blogspot.in/2015/07/working-on-issues-with-listview-in.html#WrongPosition