I am facing a very weird issue, I am showing categories to user using horizontal CollectionView in Xamarin.iOS and i am using MvvMCross as well. The issue is that that when i scroll the categories CollectionView and scroll back some of the cells are empty.
This is the code for the subclass of the MvxCollectionViewCell in which i am binding values to the UI.
public partial class DealListCategoryCollectionCellView : MvxCollectionViewCell
{
public static readonly UINib Nib = UINib.FromName("DealListCategoryCollectionCellView", NSBundle.MainBundle);
public static readonly NSString Key = new NSString("CategoryCollectionViewCell");
private FXMvxImageViewLoader _imageLoader;
public DealListCategoryCollectionCellView(IntPtr handle) : base(handle)
{
_imageLoader = new FXMvxImageViewLoader(() => CategoryImageView);
this.DelayBind(() =>
{
var set = this.CreateBindingSet<DealListCategoryCollectionCellView, CategoryCellViewModel>();
set.Bind(TitleLabel).To(vm => vm.Name);
set.Bind(_imageLoader).To(vm => vm.Slug).WithConversion("LocalImage", "icons");
set.Apply();
});
}
public static DealListCategoryCollectionCellView Create()
{
return (DealListCategoryCollectionCellView)Nib.Instantiate(null, null)[0];
}
}
And this is how i am binding the above collectionview Cell.
CategoryCollectionView.RegisterNibForCell(DealListCategoryCollectionCellView.Nib, DealListCategoryCollectionCellView.Key);
var categorySource = new MvxCollectionViewSource(CategoryCollectionView,DealListCategoryCollectionCellView.Key);
CategoryCollectionView.Source = categorySource;
And this is the screenshot of the issue :
I have fixed the issue by first changing this:
CategoryCollectionView.Source = categorySource;
to:
CategoryCollectionView.DataSource = categorySource;
So when i changed from Source to DataSource the empty cell issue was fixed but there was another issue my tap on cell was not being fired. So i implemented CollectionViewDelegate like this:
class CategoryCollectionDelegate : UICollectionViewDelegate
{
DealListViewModel viewModel;
public CategoryCollectionDelegate(DealListViewModel vm)
{
viewModel = vm;
}
public override void ItemSelected(UICollectionView collectionView, NSIndexPath indexPath)
{
Category currentCategory = viewModel.Categories[indexPath.Row];
Console.WriteLine(currentCategory.Name);
viewModel.DoSelectCategoryCommand(currentCategory);
}
}
And than i used it like that :
CategoryCollectionView.Delegate = new CategoryCollectionDelegate(DealListViewModel);
And it worked like charm :)
the solution for me was to overwrite the CellDisplayingEnded method in the CollectionViewSource. As you see here https://github.com/MvvmCross/MvvmCross/blob/develop/MvvmCross/Binding/iOS/Views/MvxBaseCollectionViewSource.cs there is some commented code in CellDisplayingEnded method which maybe in some version of mvvmcross is not commented and dataContext is set to null.
Related
I have a CalendarView that looks like this:
<CalendarView
android:layout_width="match_parent"
android:layout_height="300dp"
android:id="#+id/createReservationCalendarView" />
Here is how I handle the DateChange event without MvvmCross:
protected override void OnCreate(Bundle savedInstanceState)
{
... Code ...
calendar.DateChange += (s, args) =>
{
var year = args.Year;
var month = args.Month + 1;
var dayOfMont = args.DayOfMonth;
var date = new DateTime(year, month, dayOfMont);
var myReservations = new Intent(this, typeof(CreateReservationTimeslotScreen));
myReservations.PutExtra("selectedDate", date.Ticks);
StartActivity(myReservations);
};
}
Now that I have switched to MvvmCross, I would like to have my ViewModel start the new activity instead.
Im not sure how to do this, since the ViewModel should be OS and UI agnostic.
The "args" argument is of type CalendarView.DateChangeEventArgs, which is Android specific, so I cant use that in the ViewModel. It derives from System.EventArgs, so maybe I could use that instead. I am thinking that there must be a simpler way.
A thought that I had was if it is possible to update a property on the ViewModel from the activity, and then execute the switch to the new Activity from there? I'm not sure how this could be accomplished since activites dont have references to their ViewModels.
Any suggestions?
Thanks.
MvvmCross does give you access to your ViewModel from your View. The relationship between your View (e.g. Activity/fragment in Android) and your ViewModel, and their ability to share data (models) in both directions is a core characteristic a Mvvm framework.
In order to setup an Activity to be used with MvvmCross you need to make sure to inherit from MvxActivity or MvxAppCompatActivity (If using Android Support Library). Following which you need to link your Activity to its corresponding ViewModel using one of the possible conventions (See link, for basic sample of each registration offered by the MvxViewModelViewTypeFinder). A simple example would be to use the concrete type based registration using the type parameter overload.
public class FirstActivity : MvxAppCompatActivity<FirstViewModel>
Now that you have access to your ViewModel from your View you can create a command that can be used to execute the navigation:
CalendarViewModel (ViewModel linked to the current Activity in question)
Create a command that requires a DateTime parameter, which in turn will pass the value when navigation (see MvvmCross Navigation docs for alternative navigation and parameter passing conventions).
public class CalendarViewModel : MvxViewModel
{
IMvxCommand _goToMyReservationCommand;
public IMvxCommand GoToMyReservationCommand =>
_goToMyReservationCommand ??
(_goToMyReservationCommand = new MvxCommand<DateTime>(NavigateToMyReservation));
void NavigateToMyReservation(DateTime reservationDate)
{
ShowViewModel<MyReservationViewModel>(
new GoToMyReservationParameter
{
ReservationTicks = reservationDate.Ticks
});
}
}
Navigation Parameter Class
Holds the values and type information used for navigation.
public class GoToMyReservationParameter
{
public long ReservationTicks { get; set; }
}
MyReservationViewModel
The ViewModel that will receive the value passed.
public class MyReservationViewModel : MvxViewModel
{
public void Init(GoToMyReservationParameter parameters)
{
var reservationTicks = parameters.ReservationTicks;
// Do what you need with the parameters
}
}
View
Execute the command on the ViewModel and pass through the DateTime object.
public class CalendarActivity : MvxAppCompatActivity<CalendarViewModel>
{
protected override void OnCreate(Bundle bundle)
{
... Code...
calendar.DateChange += (s, args) =>
{
var year = args.Year;
var month = args.Month + 1;
var dayOfMont = args.DayOfMonth;
var date = new DateTime(year, month, dayOfMont);
ViewModel.GoToMyReservationCommand.Execute(date);
};
}
}
I just upgraded a project from an older version of MvvmCross to the latest version, and I'm having trouble with bindings.
I'm aware of the LinkerPleaseInclude hack, and it looks like (at least some of) the properties I'm using are listed there.
I'm concerned about my usage of the Views and ViewModels. Here's an example.
public partial class HomeView : MvxViewController<HomeViewModel>
...
public override void ViewDidLoad ()
{
base.ViewDidLoad();
this.CreateBinding(BudgetTrackerButton).To((HomeViewModel vm) => vm.BudgetTracker).Apply();
this.CreateBinding(LoginButton).For("Title").To ((HomeViewModel vm) => vm.LogMessage).Apply();
this.CreateBinding(LoginButton).To((HomeViewModel vm) => vm.Login).Apply();
this.CreateBinding(ContactApprisenButton).To((HomeViewModel vm) => vm.Contact).Apply();
this.CreateBinding(AboutApprisenButton).To((HomeViewModel vm) => vm.About).Apply();
this.CreateBinding(AboutThisAppButton).To((HomeViewModel vm) => vm.AboutApp).Apply();
this.CreateBinding(FAQsButton).To((HomeViewModel vm) => vm.FAQs).Apply();
this.CreateBinding(PrivacyPolicyButton).To((HomeViewModel vm) => vm.PrivacyPolicy).Apply();
}
public abstract class MvxViewController<T> : UIViewController, IMvxBindingContextOwner, IUIWrappable, MvvmCross.iOS.Views.IMvxIosView where T : ViewModelBase
...
}
protected MvvmCross.Core.ViewModels.IMvxViewModel _viewModel = null;
public MvvmCross.Core.ViewModels.IMvxViewModel ViewModel {
get {
if (_viewModel == null) {
_viewModel = MvvmCross.Platform.Mvx.Resolve<T> ();
}
return _viewModel;
}
set {
_viewModel = value;
}
}
public MvvmCross.Core.ViewModels.MvxViewModelRequest Request {
get;
set;
}
public object DataContext {
get {
return _viewModel;
}
set {
_viewModel = (MvvmCross.Core.ViewModels.IMvxViewModel)value;
}
}
protected MvvmCross.Binding.BindingContext.IMvxBindingContext _bindingContext;
public IMvxBindingContext BindingContext {
get {
if (_bindingContext == null) {
_bindingContext = new MvvmCross.Binding.BindingContext.MvxBindingContext ();
}
return _bindingContext;
}
set {
_bindingContext = value;
}
}
CreateBinding is from the MvxBindingContextOwnerExtensions.
When I hit CreateBinding, the view model has been made.
I understand DataContext to be the same as the ViewModel, and I only include it to conform to the MvvmCross.iOS.Views.IMvxIosView interface.
Am I missing a step somewhere? An interface?
Matching views to view models should happen automatically based on naming conventions, right? (They didn't.. in my case, I had to manually specify the mappings in my Setup class. Could be contributing to this problem.)
Strangely enough, this works (for the button title anyway, I haven't tested the other bindings, and I'm not interested in updating all the bindings across the entire application if the fix is simple):
var set = this.CreateBindingSet<HomeView, HomeViewModel>();
set.Bind(LoginButton).For("Title").To(vm => vm.LogMessage);
I can post more code if something else would be relevant. I'm also new to MvvmCross.
I am trying to create my first Xamarin.Forms custom user control named LocationSelector. It has an Entry and when the user enters something, a list with selectable locations is shown (similary to the selection in Google Maps).
The selected location is the important 'return value' of the control.
My plan is to catch the ItemSelected event of the list and set the SelectedLocation property. LocationSelector is designed as MVVM and since everything is working so far here just the Code-Behind (which I think is enough to describe the problem):
public partial class LocationSelector : StackLayout
{
public static readonly BindableProperty SelectedLocationProperty =
BindableProperty.Create<LocationSelector, LocationModel>(s => s.SelectedLocation, new LocationModel(), BindingMode.TwoWay);
public LocationSelector()
{
InitializeComponent();
var model = new LocationSelectorModel();
BindingContext = model;
_listView.ItemSelected += (sender, args) =>
{
SelectedLocation = model.SelectedLocation;
};
}
public LocationModel SelectedLocation
{
get { return (LocationModel)GetValue(SelectedLocationProperty); }
set { SetValue(SelectedLocationProperty, value); }
}
}
Now I want to use this control on a search view where the BindingContext is set to the SearchViewModel:
<ContentPage x:Class="Application.App.Views.SearchView" ...>
<c:LocationSelector SelectedLocation="{Binding Location}"/>
</ContentPage>
public class SearchViewModel : ViewModel
{
private LocationModel _location;
public LocationModel Location
{
get { return _location; }
set { SetProperty(ref _location, value); }
}
}
Unfortunately this is not working. The output throws a binding warning:
Binding: 'Location' property not found on 'Application.App.CustomControls.LocationSelectorModel', target property: 'Application.App.CustomControls.LocationSelector.SelectedLocation'
Why points the binding to a property in the ViewModel that is used 'within' my custom control and not to the property in the BindingContext of the view?
The problem is setting the BindingContext to the view model of the user control:
public LocationSelector()
{
var model = new LocationSelectorModel();
BindingContext = model; // this causes the problem
// ...
}
In this post I found the solution. Setting the BindingContext to each child elements rather than the whole user control is doing the job:
public LocationSelector()
{
var model = new LocationSelectorModel();
foreach (var child in Children)
{
child.BindingContext = model;
}
}
I am trying to bind to the badgevalue of a TabBarItem like this:
var set = this.CreateBindingSet<MyView, MyViewModel>();
set.Bind(ViewControllers[0].TabBarItem.BadgeValue).To(vm => vm.MyNumber);
set.Apply();
but I get the following error:
MvxBind: Error: 6.30 Empty binding target passed to MvxTargetBindingFactoryRegistry
However if I set the value directly like this the badge appears:
ViewControllers[0].TabBarItem.BadgeValue = ((MyViewModel)ViewModel).MyNumber;
Why is the binding not working?
Thanks!
This doesn't work because you need to set up your own binding extensions if what you are trying to bind to doesn't exist as a valid binding target already.
Look here where Stuart answered a similar question for Android.
Create a custom binding:
public class TabBadgeBinding : MvxTargetBinding
{
public const string TargetPropertyName = nameof(TabBadgeBinding);
private readonly UIViewController _viewController;
public TabBadgeBinding(UIViewController viewController) : base(viewController)
{
_viewController = viewController;
}
public override Type TargetType => typeof(UIViewController);
public override MvxBindingMode DefaultMode => MvxBindingMode.OneWay;
public override void SetValue(object value)
{
if (value is string badgeValue)
{
_viewController.TabBarItem.BadgeValue = badgeValue;
}
}
}
Register Binding in Setup.cs:
protected override void FillTargetFactories(IMvxTargetBindingFactoryRegistry registry)
{
base.FillTargetFactories(registry);
registry.RegisterFactory(new MvxCustomBindingFactory<UIViewController>(TabBadgeBinding.TargetPropertyName, (controller) => new TabBadgeBinding(controller)));
}
Use Binding in your VC:
set.Bind(ParentViewController).For(TabBadgeBinding.TargetPropertyName).To(vm => vm.UnreadNotificationsCount);
I want to make MvxTableViewCell without nib file, but I search in stackoverflow, github
and google, I couldn't good sample to make MvxTableViewCell manually.
I tried such code,
namespace KittenView.Touch
{
public partial class KittenCell : MvxTableViewCell
{
public static readonly NSString Key = new NSString ("KittenCell");
private readonly MvxImageViewLoader _imageViewLoader;
MonoTouch.UIKit.UIImageView MainImage { get; set; }
MonoTouch.UIKit.UILabel NameLabel { get; set; }
MonoTouch.UIKit.UILabel PriceLabel { get; set; }
public KittenCell () : base ()
{
this.Frame = new RectangleF(0f,0f,100f,120f);
MainImage = new UIImageView (new RectangleF(0f,0f,100f,100f));
NameLabel = new UILabel (new RectangleF (0f, 80f, 100f, 20f));
PriceLabel = new UILabel (new RectangleF (0f, 100f, 100f, 20f));
Add (MainImage);
Add (NameLabel);
Add (PriceLabel);
_imageViewLoader = new MvxImageViewLoader(() => this.MainImage);
this.DelayBind (() => {
var set = this.CreateBindingSet<KittenCell, Kitten>();
set.Bind(NameLabel).To (kitten => kitten.Name);
set.Bind(PriceLabel).To(kitten => kitten.Price);
set.Bind(_imageViewLoader).To (kitten => kitten.ImageUrl);
set.Apply ();
});
this.Transform = CGAffineTransform.MakeRotation ((float)Math.PI / 2.0f);
}
public static KittenCell Create ()
{
return new KittenCell ();
}
}
}
But this code occurs Exception in execution phase, said
SetValue:forUndefinedKey:]this calss in not key value coding-compliant for key MainImage.
at AppDelegate.cs's window.MakeKeyAndVisible().
How do I use MvxTableViewCell without nib?
Regards,
Ko-hei
The easiest way to do this is to use RegisterClassForCell to register your cell with the table view and DequeueReusableCell to then later create or reuse cells.
In order to allow UIKit to create instances of this class using this method inside the dequeue calls, then I think you need to add;
a [Register("KittenCell")] attribute in front of the class - this attribute is used by MonoTouch to tell the ObjectiveC runtime about this managed class - it allows instances of this managed class to be created from ObjectiveC,
a constructor which takes an (IntPtr handle) parameter which it passes down to the base(handle) constructor. This constructor is used to allow the managed C# object to be created alongside the underlying unmanaged UIKit object.
If you add those 2 items, I believe your celll will get created. Further, if you switch to the simpler RegisterClassForCell API, then I think you can remove the parameterless constructor and the static Create method.