Pass ItemClicked in MvxRecyclerView object to new View - c#

I'm newbie in Xamarin.
I have MvxRecyclerView to show list of Cars. Clicking in a car let user to display full specification of chosen car. I have problem with displaying full specification of chosen car in new activity (and at the same time pass object between viewmodels)
My MvxRecyclerView .xml looks like:
<mvvmcross.droid.support.v7.recyclerview.MvxRecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#android:color/background_light"
local:MvxItemTemplate="#layout/listitem_car"
local:MvxBind="ItemsSource Cars; ItemClick NavigateCommand"
/>
And my CarsViewModel with "empty" navigation to CarItemViewModel:
public class CarsViewModel : MvxViewModel
{
public CarsViewModel(IMvxNavigationService navigationService)
{
Cars = new MvxObservableCollection<Car>();
_navigationService = navigationService;
NavigateCommand = new MvxAsyncCommand(() => _navigationService.Navigate<CarItemViewModel>());
}
private MvxObservableCollection<Car> _cars;
public MvxObservableCollection<Car> Cars
{
get => _cars;
set
{
_cars = value;
RaisePropertyChanged(() => Cars);
}
}
public override async Task Initialize()
{
await base.Initialize();
CarService carService = new CarService();
await Task.Run(async () =>
{
Cars = await carService.GetCars();
});
}
private readonly IMvxNavigationService _navigationService;
public IMvxAsyncCommand NavigateCommand { get; private set; }
Do you know how is it possible to move data of chosen mvxrecyclerview item to new view using MVVMCross? Unfortunately, I don't understand what MVVMCross' documentation says about this (it looks so poor in my opinion).
I would appreciate for any help.
EDIT 1:
I've changed a bit description of my problem for more transparent.

I think the document is clear about passing parameter, here:
When you navigation, you should pass a MyObject of CarItemViewModel:
await _navigationService.Navigate<CarItemViewModel, MyObject>(new MyObject());
And then you can get the MyObject in the CarItemViewModel and use it:
public class CarItemViewModel: MvxViewModel<MyObject>
{
private MyObject _myObject;
public override void Prepare()
{
// first callback. Initialize parameter-agnostic stuff here
}
public override void Prepare(MyObject parameter)
{
// receive and store the parameter here
_myObject = parameter;
}
public override async Task Initialize()
{
await base.Initialize();
// do the heavy work here
}
}

Related

MvvmCross - Passing a string with IMvxNavigationService

I'm currently working on a Xamarin.iOS project that uses a web-api to gather data. However, I'm running into some problems trying to pass the user input from a textfield to the Tableview that gets the result from the api.
To do this I've followed the example on the MvvmCross documentation.
The problem is that the input from the Textfield never reaches the 'Filter' property in my TableviewController's viewmodel. I think I'm not passing the string object correctly to my IMvxNavigationService when called.
To clarify, in my UserinputViewController I'm binding the textfield's text like so:
[MvxFromStoryboard(StoryboardName = "Main")]
public partial class SearchEventView : MvxViewController
{
public SearchEventView (IntPtr handle) : base (handle)
{
}
public override void ViewDidLoad()
{
base.ViewDidLoad();
MvxFluentBindingDescriptionSet<SearchEventView, SearchEventViewModel> set = new MvxFluentBindingDescriptionSet<SearchEventView, SearchEventViewModel>(this);
set.Bind(btnSearch).To(vm => vm.SearchEventCommand);
set.Bind(txtSearchFilter).For(s => s.Text).To(vm => vm.SearchFilter);
set.Apply();
}
}
The Viewmodel linked to this ViewController looks like this:
public class SearchEventViewModel : MvxViewModel
{
private readonly IMvxNavigationService _navigationService;
private string _searchFilter;
public string SearchFilter
{
get { return _searchFilter; }
set { _searchFilter = value; RaisePropertyChanged(() => SearchFilter); }
}
public SearchEventViewModel(IMvxNavigationService mvxNavigationService)
{
this._navigationService = mvxNavigationService;
}
public IMvxCommand SearchEventCommand {
get {
return new MvxCommand<string>(SearchEvent);
}
}
private async void SearchEvent(string filter)
{
await _navigationService.Navigate<EventListViewModel, string>(filter);
}
}
And finally, TableviewController's viewmodel looks like this:
public class EventListViewModel : MvxViewModel<string>
{
private readonly ITicketMasterService _ticketMasterService;
private readonly IMvxNavigationService _navigationService;
private List<Event> _events;
public List<Event> Events
{
get { return _events; }
set { _events = value; RaisePropertyChanged(() => Events); }
}
private string _filter;
public string Filter
{
get { return _filter; }
set { _filter = value; RaisePropertyChanged(() => Filter); }
}
public EventListViewModel(ITicketMasterService ticketMasterService, IMvxNavigationService mvxNavigationService)
{
this._ticketMasterService = ticketMasterService;
this._navigationService = mvxNavigationService;
}
public IMvxCommand EventDetailCommand {
get {
return new MvxCommand<Event>(EventDetail);
}
}
private void EventDetail(Event detailEvent)
{
_navigationService.Navigate<EventDetailViewModel, Event>(detailEvent);
}
public override void Prepare(string parameter)
{
this.Filter = parameter;
}
public override async Task Initialize()
{
await base.Initialize();
//Do heavy work and data loading here
this.Events = await _ticketMasterService.GetEvents(Filter);
}
}
Whenever trying to run, the string object 'parameter' in my TableviewController's Prepare function remains 'null' and I have no idea how to fix it. Any help is greatly appreciated!
I believe the issue is with your command setup
new MvxCommand<string>(SearchEvent);
As this command is being bound to a standard UIButton. It will not pass through a parameter value of your filter but null instead. So the string parameter generic can be removed. Additionally, as you want to execute an asynchronous method I would suggest rather using MvxAsyncCommand
new MvxAsyncCommand(SearchEvent);
Then in terms of SearchEvent method you can remove the parameter. The value of filter is bound to your SearchFilter property. It is this property's value that you want to send as the navigation parameter.
private async Task SearchEvent()
{
await _navigationService.Navigate<EventListViewModel, string>(SearchFilter);
}

What is a simple way to show an MvxDialogFragment?

I am trying to use an MvxDialogFragment to show a data bound dialog from an activity. My Dialog ViewModel is as follows:
public class ContainerDialogViewModel : MvxViewModel
{
public string ShipperName;
public void Init(string Name)
{
ShipperName = Name;
LoadData();
}
public void LoadData()
{
Survey = SurveyDataSource.CurrSurvey;
}
private ShipmentSurvey _Survey;
public ShipmentSurvey Survey
{
get
{
return _Survey;
}
set
{
_Survey = value;
RaisePropertyChanged(() => Survey);
RaisePropertyChanged(() => Containers);
}
}
public List<ShipmentSurveyContainer> Containers
{
get
{
if (Survey == null)
return new List<ShipmentSurveyContainer>();
else
return Survey.SurveyContainers.ToList();
}
}
}
The MvxDialogFragment is coded as follows:
public class ContainerDialog : MvxDialogFragment<ContainerDialogViewModel>
{
public override Dialog OnCreateDialog(Bundle savedState)
{
base.EnsureBindingContextSet(savedState);
this.BindingInflate(Resource.Layout.ContainerDialog, null);
return base.OnCreateDialog(savedState);
}
}
In my activity, I am trying to figure out the simplest way to launch the dialog. Here is what I have tried:
public class SurveyView : MvxActivity
{
public void ShowContainerDialog()
{
ContainerDialogViewModel vm = new ViewModels.ContainerDialogViewModel();
vm.Init("Test Name");
var dialogFragment = new ContainerDialog()
{
DataContext = vm
};
dialogFragment.Show(FragmentManager, "Containers");
}
}
I'm pretty sure my method of creating the view model is unorthodox, but I don't know another way to do it. The biggest issue is that FragmentManager is cast to the wrong version. Show is looking for an Android.Support.V4.App.FragmentManager and the FragmentManager that is exposed is an Android.App.FragmentManager. I tried changing the MvxActivity to an MvxFragmentActivity, but this didn't seem to help. Can someone point me in the right direction?
MvvmCross didn't really support this when I was trying to do it, but I came across another instance where I needed this and alas, the functionality is there. Thanks to #Martijn00 for pointing me at the solution. This will be very basic, but I think it might help someone.
My Layout:
<?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="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="#style/TableHeaderTextView"
android:text="Work Date"/>
<MvxDatePicker
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="20dp"
local:MvxBind="Value WorkDate" />
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Close"
local:MvxBind="Click CloseCommand" />
</LinearLayout>
My ViewModel:
public class HoursDateDialogViewModel : MvxViewModel<EstimateHours>
{
private readonly IMvxNavigationService _navigationService;
public HoursDateDialogViewModel(IMvxNavigationService navigationService)
{
_navigationService = navigationService;
CloseCommand = new MvxAsyncCommand(async () => await _navigationService.Close(this));
}
public override System.Threading.Tasks.Task Initialize()
{
return base.Initialize();
}
public override void Prepare(EstimateHours parm)
{
base.Prepare();
Hours = parm;
}
public IMvxAsyncCommand CloseCommand { get; private set; }
private EstimateHours _Hours;
public EstimateHours Hours
{
get
{
return _Hours;
}
set
{
_Hours = value;
RaisePropertyChanged(() => Hours);
RaisePropertyChanged(() => WorkDate);
}
}
public DateTime WorkDate
{
get
{
return Hours.StartTime ?? DateTime.Today;
}
set
{
DateTime s = Hours.StartTime ?? DateTime.Today;
DateTime d = new DateTime(value.Year, value.Month, value.Day, s.Hour, s.Minute, s.Second);
Hours.StartTime = d;
DateTime e = Hours.EndTime ?? DateTime.Today;
d = new DateTime(value.Year, value.Month, value.Day, e.Hour, e.Minute, e.Second);
Hours.EndTime = d;
RaisePropertyChanged(() => WorkDate);
}
}
}
My View:
[MvxDialogFragmentPresentation]
[Register(nameof(HoursDateDialogView))]
public class HoursDateDialogView : MvxDialogFragment<HoursDateDialogViewModel>
{
public HoursDateDialogView()
{
}
protected HoursDateDialogView(IntPtr javaReference, JniHandleOwnership transfer)
: base(javaReference, transfer)
{
}
public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
var ignore = base.OnCreateView(inflater, container, savedInstanceState);
var view = this.BindingInflate(Resource.Layout.HoursDateDialogView, null);
return view;
}
}
That's all there is to it. I am able to pass a parameter object and bind part of the object to an MvxDatePicker. In order to show this dialog, first in your Setup.cs, you need :
protected override IMvxAndroidViewPresenter CreateViewPresenter()
{
return new MvxAppCompatViewPresenter(AndroidViewAssemblies);
}
In your viewmodel from which you will open the dialog, you need a constructor containing:
private readonly IMvxNavigationService _navigationService;
public LocalHourlyViewModel(IMvxNavigationService navigationService)
{
_navigationService = navigationService;
}
This injects the navigation service so you can work with it. And finally, all you have to do to open the dialog is:
async () => await _navigationService.Navigate<HoursDateDialogViewModel, EstimateHours>(Item);
I'm not even sure you have to await the call, but I was following the example. You can see more examples at the link #Martijn00 provided:
https://github.com/MvvmCross/MvvmCross/tree/develop/TestProjects/Playground
Cheers!
I tried changing the MvxActivity to an MvxFragmentActivity
This was the correct first step. Then instead of passing in FragmentManager, pass in SupportFragmentManager.
If you are not familiar with Support Libraries, You can read more about what they are and how to use them with Xamarin here

Object is not set in ViewModel class and control is directly move from data access class to View

I want to fetch first record from AboutUs table and display as a content label.
I have created 4 classes in MVVM pattern.
First is Model class AboutUs.cs
[Table("tblAboutUs")]
public class AboutUs
{
[PrimaryKey, AutoIncrement, NotNull]
public int IDP { get; set; }
public string Content { get; set; }
}
Second is Data Access class
SQLiteAboutUs.cs
public class SQLiteAboutUs
{
private static readonly AsyncLock Mutex = new AsyncLock();
private SQLiteAsyncConnection dbConn;
public int StatusCode { get; set; }
public SQLiteAboutUs(ISQLitePlatform sqlitePlatform, string dbPath)
{
if (dbConn == null)
{
var connectionFunc = new Func<SQLiteConnectionWithLock>(() =>
new SQLiteConnectionWithLock
(
sqlitePlatform,
new SQLiteConnectionString(dbPath, storeDateTimeAsTicks: false)
));
dbConn = new SQLiteAsyncConnection(connectionFunc);
dbConn.CreateTableAsync<Model.AboutUs>();
}
}
public SQLiteAboutUs()
{
}
public async Task Save(Model.AboutUs content)
{
using (await Mutex.LockAsync().ConfigureAwait(false))
{
StatusCode = 0;
await dbConn.InsertAsync(new Model.AboutUs { Content = content.Content });
StatusCode = 1;
}
//For Get first Row from Table
public async Task<Model.AboutUs> GetAllData()
{
return await dbConn.Table<Model.AboutUs>().Where(x => x.IDP == 1).FirstOrDefaultAsync();
}
}
Third class ViewModel Class
AboutUsViewModel.cs
public class AboutUsViewModel
{
readonly SQLiteAboutUs _db;
public string AboutUsContent { get; set; }
//public string AboutUs;
public AboutUsViewModel()
{
_db = new SQLiteAboutUs();
}
public async void FirstRecord()
{
Model.AboutUs obj = await _db.GetAllData();
this.AboutUsContent = obj.Content;
}
}
Forth one is Code behind file of my xaml pages.
AboutUs.xaml.cs
public partial class AboutUs : ContentPage
{
readonly AboutUsViewModel aboutUsViewModel;
public AboutUs()
{
InitializeComponent();
aboutUsViewModel = new AboutUsViewModel();
aboutUsViewModel.FirstRecord();
lblContent.Text = aboutUsViewModel.AboutUsContent;
}
}
I have debug code but problem is In AboutUsViewModel.cs class in FirstRecord Method object can not be set that's why AboutUsContent string property is also not set.
I can't figure out why my debugger directly jump from GetAllData() method in SQLiteAboutUs.cs to label.text in code behind file of view?
Welcome in the wonderfull world of asynchronicity. I encourage you to read carefully about how await is working: How and When to use `async` and `await`
It is not a blocking call. Thus you create the view AboutUs. It creates the ViewModel AboutUsViewModel. It calls
aboutUsViewModel.FirstRecord();
But does not wait for the call to be complete (dont't forget you marked your FirstRecord function as async...)
So it calls
Model.AboutUs obj = await _db.GetAllData();
And directly return to the caller because of the await operator.
That's why it directly jump to
lblContent.Text = aboutUsViewModel.AboutUsContent;
What you would like is Something like
await aboutUsViewModel.FirstRecord();
To wait the call to be complete before going to the next line. But of course you can't do that, because you are in a constructor and you can't have an async constructor. And calling a database (or actually anything that could likely failed) in a constructor is a bad practice anyway.
I would advise you to let only InitializeComponent() in the constructor, and then use Something like the OnDataLoaded() event of your view to perform your async call with a await.
Hope it helps.

Universal Apps using Unity & Prism

I am learning the whole new Universal Apps creation together with Prism and Unity, but I got a few questions I am not sure about:
I have the following simple data object:
public class Customer : IEditableObject, IEquatable<Customer>
{
private Customer backup;
public string Name { get; set; }
public string Surname { get; set; }
public DateTime DateOfBirth { get; set; }
public void BeginEdit()
{
this.backup = this.MemberwiseClone() as Customer;
}
public void CancelEdit()
{
this.Name = this.backup.Name;
this.Surname = this.backup.Surname;
this.DateOfBirth = this.backup.DateOfBirth;
}
public void EndEdit()
{
this.backup = this.MemberwiseClone() as Customer;
}
public bool WasChangeMade()
{
if (this.Equals(backup))
return false;
else
return true;
}
public bool Equals(Customer other)
{
return this.Name == other.Name &&
this.Surname == other.Surname &&
this.DateOfBirth == other.DateOfBirth;
}
}
Under my Main Page I have a simple ListBox, where I show collection of these Customers. Everything good so far.
Afterwards, when under my ListBox user selects any one of these Customer, then he can click Edit Settings button and edit properties of this selected Customer. It is a simple command:
cmd_EditCustomer = new DelegateCommand(() =>
{
_navigationService.Navigate(App.Experiences.Detail.ToString(), SelectedCustomer);
});
Which simply navigates to a new page (detail page, where user can do the changes) and the argument I pass here is the Selected Customer.
My DetailPage View Model looks like following:
public class DetailPageViewModel : ViewModel, Interfaces.IDetailPageViewModel
{
public DelegateCommand cmd_SaveChanges { get; set; }
public Customer SelectedCustomer { get; set; }
private readonly INavigationService _navigationService;
private readonly IDialogService _dialogService;
public DetailPageViewModel(INavigationService navigationService,
IDialogService dialogService)
{
_navigationService = navigationService;
_dialogService = dialogService;
InitializeCommands();
}
public override void OnNavigatedTo(object navigationParameter, NavigationMode navigationMode, Dictionary<string, object> viewModelState)
{
this.SelectedCustomer = navigationParameter as Customer;
this.SelectedCustomer?.BeginEdit();
}
private void InitializeCommands()
{
cmd_SaveChanges = new DelegateCommand(() =>
{
SelectedCustomer?.EndEdit();
_dialogService.Show("Changes Saved!");
_navigationService.Navigate(App.Experiences.Main.ToString(), null);
});
}
}
As you can see, this is a very simple application, which I only use for learning purposes. Here are my questions:
1) Is it good to pass Selected Customer in such a way as I did? (in the parameter of the INavigationService), or should I implement other logic?
2) When user makes a change to the Selected Customer and clicks Save Changes (the only command you can see there), it does not update the original Customer (from my original collection). How is this possible? How to achieve, that my Customer will be updated? Should I create PubSubEvent for this?
EDIT:
I have managed to locate the error - when user navigates back to MainPage, my MainPageViewModel is re-initializes, which re-populates collection of items. The question now is - how can I keep MainWindowViewModel alive thorough the applications life?
Re-populates collection of items from what?
You just need to save a new values, for example if you populate your customers from DB you have to call DB and save changes before navigate back etc, so after that when MainPageViewModel would be re-initializes you'll get your changes and changes performed by another users.
In the end, I found out that this was not a good way how to hold data in your application.
Based on what I have read, I should have implemented Repository Strategy, which is only referenced in a ViewModel such as:
public MainPageViewModel(IDataRepository dataRepository, INavigationService navService, ...){etc.}
Example of a simplified interface:
public interface IDataRepository
{
List<string> GetListOfStrings();
string GetUserEnteredData();
void SetUserEnteredData(string data);
}
This is how you initialize it in UnityContainer:
_container.RegisterType<IDataRepository, DataRepository>();
You can read more from Patterns & Practices team in here:
https://prismwindowsruntime.codeplex.com/

Nancy: Modify model in AfterRequest event?

I want to add an AfterRequest event handler to my Bootstrapper.cs that is able to modify the model on the Response after every route is invoked. Is this possible? I don't see any properties on the Response where I could gain access to the Model (if there is one).
Here is my example usage (from Bootstrapper.cs):
protected override void ApplicationStartup(..., IPipelines pipelines)
{
...
pipelines.AfterRequest += ModifyModel;
}
private void ModifyModel(NancyContext ctx)
{
// do things to the response model here
}
If you're still need this functionality you may be interested in an extension I just published on Nuget: https://www.nuget.org/packages/Nancy.ModelPostprocess.Fody. We needed a similar functionality in our project
This will allow you to modify your models after the route has already executed. Do have a look at the description on the Bitbucket page
Please tell me if this suits your needs.
I think is not that simple, you should inspect the ctx.Response.Content in order to know which deserializer is used and what object are you returning, I made a simple example returning a Foo Object Serialized as Json.....
public class MyBootstrapper : Nancy.DefaultNancyBootstrapper
{
protected override void ApplicationStartup(TinyIoC.TinyIoCContainer container, Nancy.Bootstrapper.IPipelines pipelines)
{
base.ApplicationStartup(container, pipelines);
pipelines.AfterRequest += ModifyModel;
}
private void ModifyModel(NancyContext ctx)
{
Foo foo;
using(var memory = new MemoryStream())
{
ctx.Response.Contents.Invoke(memory);
var str = Encoding.UTF8.GetString(memory.ToArray());
foo = JsonConvert.DeserializeObject<Foo>(str);
}
ctx.Response.Contents = stream =>
{
using (var writer = new StreamWriter(stream))
{
foo.Code = 999;
writer.Write(JsonConvert.SerializeObject(foo));
}
};
}
}
public class HomeModule : Nancy.NancyModule
{
public HomeModule()
{
Get["/"] = parameters => {
return Response.AsJson<Foo>(new Foo { Bar = "Bar" });
};
}
}
public class Foo
{
public string Bar { get; set; }
public int Code { get; set; }
}
After researching this more, this is simply not possible (at least within reason) to do with the Nancy framework as it exists today.

Categories

Resources