I'm trying to bind to a Spinner using ReactiveUI in a Xamarin.Android application. To add items to the Spinner, I need to use ArrayAdapter. But ArrayAdapter needs Android.Content.Context. Should I pass it into the ViewModel?
Anyone know about an application written in Xamarin.Android, which uses ReactiveUI, where I could look inspiration? The ReactiveUI documentation only has a reference to a sample application written for iOS.
View
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<TextView
android:id="#+id/Label"
android:text="Zařízení:"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<Spinner
android:id="#+id/Devices"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="#id/Label"/>
<Button
android:id="#+id/EditCommand"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="#+id/Devices"
android:text="Edit"/>
</RelativeLayout>
Activity
namespace Test.Droid
{
[Activity(Label = "Test.Droid", MainLauncher = true)]
public class MainActivity : ReactiveActivity, IViewFor<MainViewModel>
{
public Spinner Devices { get; private set; }
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
SetContentView(Resource.Layout.Main);
ViewModel = new MainViewModel();
this.WireUpControls();
// Bindings
this.Bind(this.ViewModel, ) // ?
}
private MainViewModel _viewModel;
public MainViewModel ViewModel
{ get => _viewModel; set { this.RaiseAndSetIfChanged(ref _viewModel, value); } }
object IViewFor.ViewModel
{ get => ViewModel; set { ViewModel = (MainViewModel)value; } }
}
}
ViewModel
namespace Test.Droid.ViewModels
{
public class MainViewModel : ReactiveObject
{
// How to databind Spinner Devices ?
public MainViewModel()
{
}
}
}
I haven't done any Xamarin.Android development, but in general you don't want to pass details about the view into the ViewModel - it should not know anything about the view.
I would expose the list of items as a collection (e.g. IList<Item>) and use a converter on the binding to create an ArrayAdapter:
this.OneWayBind(this.ViewModel.Devices, this.View.Property, devices => new ArrayAdapter(devices));
this.View.Property should refer to the property that changes whenever the list of devices changes. The third parameter (devices => new ArrayAdapter()) receives the property from the ViewModel as an argument, you then return a value that can be set on the this.View.Property.
For example:
ViewModel.Count is a string
View.Property is an int
Bind like this:
this.OneWayBind(this.ViewModel.Count, this.View.Property, count => int.Parse(count));
The third parameter can be a function or lambda that accepts an argument of the type of the ViewModel property and returns a value of the type of the view property.
I'd said I'd given in providing any feedback on Stackoverflow but anyway... we have a collection of binding extensions, one of which is for a Spinner. in typical usage it looks like this
// bind the options for the distance
this.BindSpinner(ViewModel,
vm => vm.TravelLimitSelected,
vm => vm.CurrentTravelLimit,
vm => vm.TravelLimitChoices.Distances,
f => f.travelLimitSpinner,
(items) =>
new ActionSpinnerAdapter<DistanceChoiceModel>((c, p) => new DistanceLimitViewHost(c, p), items));
where in this case, the extension method looks like
public static IDisposable BindSpinner<TView, TViewModel, TCommandProp, TSpinnerViewModel>(
this TView view,
TViewModel viewModel,
Expression<Func<TViewModel, TCommandProp>> viewModelCommandName,
Expression<Func<TViewModel, int>> viewModelSelectedPropertyName,
Expression<Func<TViewModel, IList<TSpinnerViewModel>>> viewModelSourceItemsName,
Expression<Func<TView, Spinner>> spinnerControl, Func<IList<TSpinnerViewModel>, ISpinnerAdapter> adapterFactory) where TViewModel : RxViewModel
where TCommandProp : ICommand
where TView : class, IViewFor<TViewModel>
where TSpinnerViewModel : class
{
Related
This's my first question here, so hi everybody.
I'm working on the mobile app in Xamarin.Forms with Prism. I've created ListView where shown data from the database.
When the user clicks in the selected row app should navigate to a new view and pass the selected item from ListView.
<ListView x:Name="DefectsBase"
RowHeight="65"
ItemsSource="{Binding Defects}"
ItemSelected="ShowDetailsEvent"
IsPullToRefreshEnabled="true"
RefreshCommand="{Binding Refresh}"
IsRefreshing="{Binding IsRefreshing}">
Code backend:
async void ShowDetailsEvent(object sender, EventArgs e)
{
var myListView = (ListView)sender;
var myItem = myListView.SelectedItem;
var p = new NavigationParameters();
p.Add("selectedDefect", myItem);
await _navigationService.NavigateAsync("DefectDetailsView", p);
}
Unfortunately, the app doesn't respond to pressing the selected row in ListView.
As I can see you are already using Prism and you have a List page with Items and you want to navigate to some details page based on the selected/taped/chosen item which the user taps in the ListView.
The idea is to move as much code and logic as we can to the view model and keep our code-behind. This is pretty easy to solve using Prism and EventToCommand behaviour.
In the example and answer below, I will show you how to solve this with few lines of code, with a nice code approach.
First of all, I recommend you use EventToCommand behaviour, you can include it with prism xmlns, like this: xmlns:prism="http://prismlibrary.com", later on, you can use it with ListView.
Remove ItemSelected event from your ListView and move the markup about it to the <ListView.Behaviors> part. Here is my code sample for the ListView which binds to some ObserverableCollection of the Car models:
<ListView ItemsSource="{Binding Cars}">
<ListView.ItemTemplate>
<DataTemplate>
...
</DataTemplate>
</ListView.ItemTemplate>
<ListView.Behaviors>
<prism:EventToCommandBehavior EventName="ItemTapped"
Command="{Binding SelectedCarCommand}"
EventArgsParameterPath="Item" />
</ListView.Behaviors>
The main part here is <ListView.Behaviors>, where you can see that I am binding to the SelectedCarCommand which will be invoked when the user taps on some of the items from the list. I am using the ItemTapped event for this and passing the current "taped" item from the list as a parameter.
In order to follow this XAML part in my view model of this page, I have declared the DelegateCommand and method which will be called when the command is invoked. The view model part looks like this:
This is my CarListPageViewModel, take a look at DelegateCommand and SelectedCar method.
public class CarListPageViewModel
{
private readonly INavigationService _navigationService;
public ObservableCollection<Car> Cars { get; set; }
public DelegateCommand<Car> SelectedCarCommand { get; private set; }
public CarListPageViewModel(INavigationService navigationService, IDataProvider dataProvider)
{
_navigationService = navigationService;
// Insert test data into collection of Cars
Cars = new ObservableCollection<Car>(dataProvider.GetData());
SelectedCarCommand = new DelegateCommand<Car>(SelectedCar);
}
private async void SelectedCar(Car selectedCar)
{
NavigationParameters navigationParameters = new NavigationParameters
{
{ "selectedCar", selectedCar }
};
await _navigationService.NavigateAsync(nameof(CarDetailsPage), navigationParameters);
}
}
As you can see we have DelegateCommand defined with the type of parameter which will be passed, in my case, this is the Car class, the same class as our items in the ListView.
In the constructor, I did my initialization and defined the method which will be called, that method has a parameter of the type Car.
When the user taps on one of the items in the ListView, SelectedCar (method) will be called and we can pass the data to the next view using NavigationParameters and NavigationService.
In order to retrieve the passed data we can use INavigationAware in the details view model and with the OnNavigatedTo method, access the data which is being passed.
This is my CarDetailsPageViewModel, take a look at OnNavigatedTo method.
public class CarDetailsPageViewModel : BindableBase, INavigationAware
{
private string carTitle;
public string CarTitle
{
get { return carTitle; }
set { SetProperty(ref carTitle, value); }
}
private string photoUrl;
public string PhotoUrl
{
get { return photoUrl; }
set { SetProperty(ref photoUrl, value); }
}
public CarDetailsPageViewModel() { }
public void OnNavigatedTo(INavigationParameters parameters)
{
if (parameters.ContainsKey("selectedCar"))
{
Car car = parameters.GetValue<Car>("selectedCar");
if (car != null)
{
CarTitle = $"{car.Make} {car.Model}";
PhotoUrl = car.PhotoUrl;
}
}
}
public void OnNavigatedFrom(INavigationParameters parameters) { }
}
From this answer and example, you can see:
How to, use EventToCommand behaviour with ListView
Define and use DelegateCommand with passing parameter
How to navigate to another view and pass navigation parameter and
... finally how to access the passed data.
Code and this sample you can find on my GitHub profile here.
Hope this answer was helpful for you!
Wishing you lots of luck with coding! 👋
So I have a class MainViewModel in which I have a button. The button navigates to a view which has its own view model, let's call in ListViewModel. It resides inside MainViewModel. It has an ObservableCollection called WorkOrders.
In my main view model, I have a property, which returns the number of items, in the list in my ListViewModel. However, if I bind my button text to this property (NumberOfWorkOrders), then nothing happens, when WorkOrders.Count() changes. Even if I call OnPropertyChanged("NumberOfWorkOrders").
However, it does work, if I bind to the identical property inside the ListViewModel. How come it does not work, with the property in the MainViewModel? Is it because the notification from INotifyPropertyChanged does not work in a different view model?
Button binding which DOES NOT work (uses property from MainViewModel)
<Button
Content="{Binding NumberOfWorkOrders}"
ContentStringFormat="WorkOrders ({0})" />
Button binding which DOES work (uses property from ListViewModel)
<Button
DataContext="{Binding LVM}"
Content="{Binding NumberOfWorkOrders}"
ContentStringFormat="WorkOrders ({0})" />
MainViewModel.cs
public class MainViewModel : BindableBase
{
// INotifyPropertiesChanged is implemented in BindableBase
private ListViewModel listViewModel = new ListViewModel();
// This is where I would like to bind my button text to
public int NumberOfWorkOrders
{
get { return listViewModel.WorkOrders.Count(); }
}
// I would prefer not to have this
public ListViewModel LVM
{
get { return listViewModel; }
}
}
ListViewModel.cs
public class ListViewModel : BindableBase
{
// INotifyPropertiesChanged is implemented in BindableBase
public ObservableCollection<WorkOrder> WorkOrders
{
get; set;
}
// I would prefer to use the version of this in the MainViewModel
public int NumberOfWorkOrders
{
get { return listViewModel.WorkOrders.Count(); }
}
public void RefreshWorkOrders()
{
(...) // Load work orders and add them to collection
OnPropertyChanged("NumberOfWorkOrders");
}
}
You are running into this problem
, where you have aggregated property which required additional job: you will have to subscribe to ListViewModel.PropertyChanged and rise notification for MainViewModel.NumberOfWorkOrders property:
public class MainViewModel : BindableBase
{
readonly ListViewModel listViewModel = new ListViewModel();
public int NumberOfWorkOrders => listViewModel.WorkOrders.Count();
public MainViewModel()
{
// since you already rise notification in ListViewModel
// listen to it and "propagate"
listViewModel.PropertyChanged += (s, e) =>
{
if(e.PropertyName == nameof(ListViewModel.NumberOfWorkOrders))
OnPropertyChanged(nameof(NumberOfWorkOrders));
};
}
}
First button (which does NOT work) has MainViewModel as data context, binding will only listen for notification in this class.
As a simple fix you can include LVM in Path of binding. Binding is smart and will start listening to both events: of main view model and of that instance given by LVM property:
<Button Content="{Binding LVM.NumberOfWorkOrders}" ... />
you can delete NumberOfWorkOrders from MainViewModel then.
The reason your MainViewModel binding doesn't work, because you call OnPropertyChanged("NumberOfWorkOrders") from ListViewModel context.
I would suggest in MainViewModel to sign to changes in listViewModel.WorkOrders, and fire OnPropertyChanged("NumberOfWorkOrders") from MainViewModel, when WorkOrders changed. You need to look into documentation of ObservableCollection to find how to sign for collection changed notifications.
I have written a Windows Store App that I need to port to Android. I am attempting to use MvvmCross and Xamarin in Visual Studio to achieve this. In my Windows App, I would create a screen using XAML and in the textbox etc. set the binding to the field in my datamodel object. I would get my datamodel objects from a WCF service reference. In the code behind for the screen, I would just set the datacontext of the root layout grid to the datamodel object generated by the Service Reference. It was pretty simple.
In MvvmCross, it seems that you basically run the viewmodel in order to load a page. The syntax for the fields in the viewmodel are really identical to the ones generated in the datamodel by the service reference. I know that Mvvm needs the viewmodel as the shim between the datamodel and the view. Is there an efficient way to pass the properties from the datamodel, through the viewmodel to the view? I have the service reference working and generating objects and data from the WCF. I could hard code each field that exists in the datamodel into the viewmodel and have the get set act on fields from the datamodel object. I was just hoping there was a less manual way to do it. Any suggestions?
#Stuart had an excellent suggestion. Here is what I did. Here is my ViewModel:
public class InventoryViewModel
: MvxViewModel
{
public async void Init(Guid ID)
{
await MPS_Mobile_Driver.Droid.DataModel.ShipmentDataSource.GetShipmentInventory(ID);
ShipmentInventory = ShipmentDataSource.CurrInventory;
Shipment = await MPS_Mobile_Driver.Droid.DataModel.ShipmentDataSource.GetShipment((int)ShipmentInventory.idno, (short)ShipmentInventory.idsub);
}
private Shipment _Shipment;
public Shipment Shipment
{
get { return _Shipment; }
set { _Shipment = value; RaisePropertyChanged(() => Shipment); }
}
private ShipmentInventory _ShipmentInventory;
public ShipmentInventory ShipmentInventory
{
get { return _ShipmentInventory; }
set { _ShipmentInventory = value; RaisePropertyChanged(() => ShipmentInventory); }
}
}
I pass it a Guid ID and in the Init method, it gets the Shipment Inventory and the Associated Shipment. When I bind the fields, I just bind to Shipment. as follows:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:local="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<EditText
android:layout_width="fill_parent"
android:layout_height="wrap_content"
style="#style/InputEditText"
local:MvxBind="Text Shipment.OrgEmail" />
</LinearLayout>
That's all there was to it!
Hope this helps someone.
Jim
I am new to Windows Phone development.
I am writing an application for Windows Phone 8 using MVVM Light Toolkit.
I have a MainPage with longlistselector navigating to the details page with the relaycommand and everything is good.
Now in the detail page I have to fill out the UI controls with the binding context received from the MailPage (selecteditem of the longlistselector). My problem is that I have in the detail page and which selecteditem should be bound to the data context received from the mainpage.
Just to give an example in the mainpage I have the lostlingselector bound to a list of task objects of the mainviewmodel; every task have its own category which could be selected from the availabe task categories. How could I approach this? Is it possible to bound the ItemSource of the ListPicker control in the detail page to a different viewmodel and the SelectedItem of the same control to the proprties Category of the default viewmodel (selected task object)?
Thank you.
You can create a new view with it's own viewmodel and pass the data between viewmodels using the MVVM Light Messenger class.
Something like:
public class DetailsViewModel
{
public DetailsViewModel()
{
Messenger.Default.Register<Item>(this, "ItemDetails", i=> ViewItemDetails(i));
}
public void ViewItemDetails(Item i)
{
//Now you can bind it to your UI
}
}
And pass the object from your main viewmodel just like this (ItemDetails it's just a token to identify the listeners)
Messenger.Default.Send<Item>(SelectedItem, "ItemDetails");
You shouldn't really mess up with bindings in between different ViewModel. To setup interaction between viewmodel's you can use Messnger from MvvmLight toolkit, inject it in both objects and define proper pub-sub relations.
public class FirstViewModel : INotifyPropertyChanged
{
private IMesseger messenger;
...
public FirstViewModel(IMessenger messenger)
{
this.messenger = messenger;
}
public Item SelectedItem
{
get
{
return this.selectedItem;
}
set
{
this.selectedItem = value;
this.messenger.Send(new GenericMessage<Item>(this.selectedItem));
this.OnPropertyChanged("SelectedItem");
}
}
}
public class SecondViewModel : INotifyPropertyChanged
{
private IMesseger messenger;
...
public SecondViewModel(IMessenger messenger)
{
this.messenger = messeger;
this.messenger.Register<GenericMessage<Item>>(this, this.HandleItemSelected);
}
...
}
So after sharing same instance of messenger between two VM's you'll have desired functionality with loosely coupled relations which is good from testing perspectives.
I am using Xamarin to develop an App for Android in C# using MvvmCross.
The app compiles and runs fine when I am using one of my View Models which is bind to a seekbar, but doesn't with the View Models that I want to bind with a button. All my View models have a very similar structure (pretty much identical besides the names).
A sample of my setup is presented below:
My View Model looks something like:
public interface ISomething
{
int MethodSomething(int i);
}
public class ChangeSomethingViewModel : MvxViewModel
{
private readonly ISomething _somethign;
public ChangeSomethingViewModel(ISomething something)
{
_something = something;
}
public override void Start()
{
Terminate_Something = "hello";
base.Start();
}
.
.
.
public string terminate;
public string Terminate_Something
{
get { return terminate; }
set { terminate= "pressed"; RaisePropertyChanged(() => Terminate_Something); }
}
I then have another file with MethodSomething(int i)
public class ChangeSomething : ISystem
{
public int MethodSomething(int i)
{
.
.
.
}
}
In setup.cs
protected override IMvxApplication CreateApp()
{
Mvx.RegisterSingleton<IMvxAppStart>(new MvxAppStart<ChangeSomethingViewModel>());
return new App();
}
protected override void InitializeFirstChance()
{
Mvx.RegisterSingleton<ISomething>(new ChangeSomething());
base.InitializeFirstChance();
}
In my Main.axml file I have a button
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
local:MvxBind="Click Terminate_Something" />
When I run the app Terminate_Something is set and displayed in the button, as I would expect. However, if I place a breakpoint in Terminate_Something and click on the button no call to Terminate_Something is made.
I should probably mention that I can see in the log the following every time I touch the button.
04-17 12:57:14.070 D/GestureDetector( 5928): [Surface Touch Event] mSweepDown False, mLRSDCnt : -1 mTouchCnt : 7 mFalseSizeCnt:0
I can't quite understand what can be the problem as my other view model works great linking to a slider. When I press on the slider (seekbar) moves and the calls Set, the corresponding log is
04-17 12:57:45.060 D/ProgressBar( 5928): setProgress = 43, fromUser = true
04-17 12:57:45.060 D/ProgressBar( 5928): mProgress = 50mIndeterminate = false, mMin = 0, mMax = 70
Thank you in advance
It looks like the problem is because you are not binding to an ICommand.
Also, I might be missing it somewhere, but you should also specify in Terminate_Something MethodSomething, no?
This should fix it: In your View Model change your Terminate_Something property to
private MvxCommand terminate;
public System.Windows.Input.ICommand Terminate_Something
{
get
{
terminate = terminate ?? new MvxCommand(MethodSomething);
return terminate;
}
}
Since you mentioned that the SeekBar is working didn't you do something like?
set { terminate= "pressed"; RaisePropertyChanged(() => Terminate_Something); MethodSomething();}
In any case Stuart has a video pretty much on this very thing in N=5
EDIT:
Obviously, you can't set terminate to "hello" in start()... depending on what you are trying to do with the button you can bind the text "hello" of the button to a different property.