Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Passing data to fragment viewmodel mvvmcross

so I have an activity that has a tabbed page with 2 fragments.

  public class RecipeDetailActivity : BaseFragmentActivity<RecipeDetailViewModel>
    {
        protected override void OnCreate(Bundle savedInstanceState)
        {
            base.OnCreate(savedInstanceState);

            SetContentView(Resource.Layout.RecipeDetailView);

            AttachActionBar();
            SupportActionBar.SetDisplayHomeAsUpEnabled(true);
            SupportActionBar.Title = "Recipe details";

            var viewPager = FindViewById<ViewPager>(Resource.Id.main_view_pager);

            if (viewPager != null)
            {
                var fragments = new List<MvxViewPagerFragmentInfo>();
                fragments.Add(
                    new MvxViewPagerFragmentInfo("Ingrediente", typeof(RecipeFlavoursFragment), typeof(RecipeFlavoursViewModel)));
                fragments.Add(
                    new MvxViewPagerFragmentInfo("Flavours", typeof(RecipeIngridientsFragment), typeof(RecipeIngridientsViewModel)));

                viewPager.Adapter = new MvxFragmentPagerAdapter(this, SupportFragmentManager, fragments);


                viewPager.Adapter = new MvxFragmentPagerAdapter(this, SupportFragmentManager, fragments);
                var tabLayout = FindViewById<TabLayout>(Resource.Id.main_tablayout);
                tabLayout.SetupWithViewPager(viewPager);
            }
        }
    }

I show this page using the following code.

   private void SelectRecipe(RecipeModel recipe)
   {
            var recipeJson = JsonConvert.SerializeObject(recipe);

            ShowViewModel<RecipeDetailViewModel>(new { recipe = recipeJson });
   }

Now what I would like is to pass some data to child view models. RecipeFlavoursViewModel RecipeIngridientsViewModel

I've tried so far : Using parameterValueObject

        fragments.Add(
            new MvxViewPagerFragmentInfo("Ingrediente", typeof(RecipeFlavoursFragment), typeof(RecipeFlavoursViewModel), new { recipe = ViewModel.Recipe }));

Using IMvxBundle

In RecipeDetailViewModel

 protected override void SaveStateToBundle(IMvxBundle bundle)
 {
            bundle.Data["Recipe"] = JsonConvert.SerializeObject(Recipe);

            base.SaveStateToBundle(bundle);
 }

In RecipeIngridientsViewModel

protected override void InitFromBundle(IMvxBundle parameters)
        {
            base.InitFromBundle(parameters);

            if (parameters.Data.Count != 0)
            {
                Recipe = JsonConvert.DeserializeObject<RecipeModel>(parameters.Data["recipe"]);
            }
        }

None of them have worked so far. Any ideas what am I doing wrong? Do I have to use the navigation service from MvvmCross 5 to be able to use InitFromBundle and SaveStateToBundle.

InitFromBundle it's called everytime my fragments is displayed, but SaveStateToBundle from RecipeDetailViewModel never gets called.

like image 584
CiucaS Avatar asked Feb 13 '18 17:02

CiucaS


1 Answers

In order to do this, you could take advantage of the MvxViewPagerFragmentPresentationAttribute so that the Presenter takes the responsibility to show the fragments and you just show the ViewModels passing the Recipe parameter as any other but it has some minor bugs for the moment.

However one way to solve this is to have in your RecipeDetailViewModel properties with the fragments' ViewModels you want to have in your ViewPager and load them in the Initialize so then you can reference them from your RecipeDetailActivity:

Using Mvx 5 you can use new Navigation to show the ViewModels. If the details are openned from RecipeListViewModel then:

public class RecipeDetailViewModelArgs
{
    public RecipeDetailViewModelArgs(RecipeModel recipe)
    {
        this.Recipe = recipe;
    }

    public RecipeModel Recipe { get; }
}

public class RecipeListViewModel : MvxViewModel
{
    private readonly IMvxNavigationService navigationService;

    public RecipeListViewModel(IMvxNavigationService navigationService)
    {
        this.navigationService = navigationService;
    }

    private async Task SelectRecipe(RecipeModel recipe)
    {
        await this.navigationService.Navigate<RecipeDetailViewModel, RecipeDetailViewModelArgs>(new RecipeDetailViewModelArgs(recipe));
    }
}

Then in your details ViewModel you just cache the recipe, load the children ViewModels (ingredients and flavours) and set the recipe to them:

public class RecipeDetailViewModel : MvxViewModel<RecipeDetailViewModelArgs>
{
    private readonly IMvxViewModelLoader mvxViewModelLoader;
    private readonly IMvxJsonConverter jsonConverter;
    private RecipeModel recipe;

    public RecipeDetailViewModel(IMvxViewModelLoader mvxViewModelLoader, IMvxJsonConverter jsonConverter)
    {
        this.mvxViewModelLoader = mvxViewModelLoader;
        this.jsonConverter = jsonConverter;
    }

    public override void Prepare(RecipeDetailViewModelArgs parameter)
    {
        this.recipe = parameter.Recipe;
    }

    protected override void SaveStateToBundle(IMvxBundle bundle)
    {
        base.SaveStateToBundle(bundle);

        bundle.Data["RecipeKey"] = this.jsonConverter.SerializeObject(this.recipe);
    }

    protected override void ReloadFromBundle(IMvxBundle state)
    {
        base.ReloadFromBundle(state);

        this.recipe = this.jsonConverter.DeserializeObject<RecipeModel>(state.Data["RecipeKey"]);
    }

    public override async Task Initialize()
    {
        await base.Initialize();

        this.InitializeChildrenViewModels();
    }

    public RecipeFlavoursViewModel FlavoursViewModel { get; private set; }

    public RecipeIngridientsViewModel IngredientsViewModel { get; private set; }

    protected virtual void InitializeChildrenViewModels()
    {
        // Load each childre ViewModel and set the recipe
        this.FlavoursViewModel = this.mvxViewModelLoader.LoadViewModel(new MvxViewModelRequest<RecipeFlavoursViewModel>(null, null), null);
        this.FlavoursViewModel.Recipe = this.recipe;

        this.IngredientsViewModel = this.mvxViewModelLoader.LoadViewModel(new MvxViewModelRequest<RecipeIngridientsViewModel>(null, null), null);
        this.FlavoursViewModel.Recipe = this.recipe;
    }
}

Then when you load the ViewPager you can take advantage from the other constructor of MvxViewPagerFragmentInfo => public MvxViewPagerFragmentInfo (string title, string tag, Type fragmentType, IMvxViewModel viewModel, object parameterValuesObject = null) so you can pass the ViewModels previously loaded:

this.viewPager = view.FindViewById<ViewPager>(Resource.Id.viewPagerDetails);
if (viewPager != null)
{
    var fragments = new List<MvxViewPagerFragmentInfo>();
    fragments.Add(new MvxViewPagerFragmentInfo("Ingredients", "RecipeIngridientsViewModelTag", typeof(RecipeIngridientsView), this.ViewModel.IngridientsViewModel));
    fragments.Add(new MvxViewPagerFragmentInfo("Flavours", "RecipeFlavoursViewModelTag", typeof(RecipeFlavoursView), this.ViewModel.FlavoursViewModel));

    this.viewPager.Adapter = new MvxFragmentPagerAdapter(this.Activity, this.ChildFragmentManager, fragments);
}

That's it.


BTW if you don't want to use Navigation or you are not using Mvx 5.x then you just Initialize the children ViewModels in the void Start() method.

And to conclude if you want to change values of your Recipe from the children one simple way is to have a Singleton initialized with your Recipe and then you just Inject the singleton in the constructors so you always have the reference to the same Recipe and you don't have to pass the Recipe object forth & back to those ViewModels and merge the changes made from each of them. More info in MvvmCross: Accessing models by reference from everywhere

HIH

like image 158
fmaccaroni Avatar answered Nov 01 '22 13:11

fmaccaroni