Xamarin MvvmCross MvxAppCompatDialogFragment looses BindingContext - xamarin

I've been using MvxAppCompatActivity throughout my project, but in this specific case I have to make use of a MvxAppCompatDialogFragment.
Unfortunately, in this case I lose the binding context of the ViewModel somehow.
MobileTestView
[MvxDialogFragmentPresentation]
[Register(nameof(MobileScreenTestView))]
public class MobileTestView : MvxAppCompatDialogFragment<MobileTestViewModel>
...
public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
return inflater.Inflate(Resource.Layout.mobile_screen, container, false);
}
...
MobileTestViewModel
public class MobileTestViewModel : MvxViewModel<MInput, MResult>
...
public string Instructions { get; set; } = "Instructions";
...
mobile_screen.axml
...
<TextView
android:id="#+id/text_mobile"
android:layout_width="match_parent"
android:layout_height="44dp"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:gravity="center_vertical|center_horizontal"
tools:text="Scan"
local:MvxBind="Text Instructions" />
...
local:MvxBind="Text Instructions" does not work anymore, but I've checked and it is set in the view model before it gets to OnCreateView().
The above code would work fine for a MvxAppCompatActivity.
If what I'm trying to do is not possible, I can always do it like
view.FindViewById<TextView>(Resource.Id.text_mobile).Text = ViewModel.Instructions;
It's not that I really need to use local:MvxBind, but I would like to know what I'm doing wrong.
Update - For anyone having the same problem:
Change the OnCreateView method to:
public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
base.OnCreateView(inflater, container, savedInstanceState);
return this.BindingInflate(Resource.Layout.mobile_screen, container, false);
}
and your BindingContext will work fine.

As you noticed yourself, you had to use this.BindingInflate instead of the LayoutInflater argument in OnCreateView. This is because we have no way to intercept the Fragment lifecycle in MvvmCross to provide our own Layout Inflater.
What BindingInflate does, is to run through the view hierarchy and look for all the custom attributes applied on views, in your case Text Instructions and apply these bindings between the View and the ViewModel.
So whenever working with Fragments, you should use BindingInflate.

Related

Automatically persist changes from two way data binding using Room?

Question
Two-Way DataBinding allows you to automatically populate UI components with data from an Object and then automatically update the Object as the user edits those UI components.
As the user edits UI components, is there a way to not only automatically update the Object in memory, but automatically update/persist the Object in a Room database?
I could manually listen to every UI field modification and manually save the Object to the Room Database. However, such a manual, brute force approach would negate the benefits of Two-Way DataBindings that I'm hoping to utilize.
Context
My application stores Items in an SQLite database using Android's Room Persistence Library. This is a simplified version of my Item:
#Entity
public class Item {
#PrimaryKey(autoGenerate = true)
private long id;
private String name;
public long getId() {
return this.id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
The magic of ViewModel, LiveData, and Two-Way DataBindings allows my ItemEditorFragment to automatically populate the UI with data from the selected Item and to update that Item when the user edits the UI components:
#Override
public void onAttach(Context context) {
super.onAttach(context);
if (context instanceof ItemViewModelProvider) {
final ItemViewModelProvider itemViewModelProvider = (ItemViewModelProvider) context;
mViewModel = itemViewModelProvider.getItemViewModel();
} else {
throw new RuntimeException(context.toString()
+ " must implement ItemViewModelProvider");
}
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final FragmentItemEditorBinding binding = DataBindingUtil.inflate(inflater, R.layout.fragment_item_editor, container, false);
binding.setViewModel(mViewModel);
binding.setLifecycleOwner(this);
final View view = binding.getRoot();
return view;
}
This is a simplified version of the layout being inflated:
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="com.lafave.ItemViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#={viewModel.selectedItem.name}"
android:layout_gravity="center_horizontal"/>
</LinearLayout>
</layout>
One of the dirty hacks I've made work is: creating a new property, in the ViewModel (but it doesn't strictly have to be exactly there), without a backing field (in Java, this would probably equate to creating a getter and a setter, without a field):
var selectedItemName: String?
get() = selectedItem.value?.name
set(value) {
selectedItem.value?.let {
it.name = value
dao.update(it)
}
}
This is not an ideal solution, since it requires mirroring all of the properties in the class, but it works if you just want to get it over with. I'm awaiting a better solution!

MvvmCross creating custom binding to LinearLayout

I am trying to create a custom binding to LinearLayout to allow me create a view dynamically and bind this as a child to LinearLayout. For anyone familiar with WPF, this is similar to the functionality provided by the ContentControl or any WPF control deriving from ContentControl. Basically you can create your dynamic content and bind to the Content property of a ContentControl.
Here's what I have for the custom binding:
public class MvxLinearLayoutContentTargetBinding : MvxPropertyInfoTargetBinding<LinearLayout>
{
public MvxLinearLayoutContentTargetBinding(object target, PropertyInfo targetPropertyInfo) : base(target, targetPropertyInfo)
{
}
protected override void SetValueImpl(object target, object value)
{
base.SetValueImpl(target, value);
var view = target as LinearLayout;
if (view == null) return;
view.AddView((View)value);
}
public override Type TargetType
{
get { return typeof(LinearLayout); }
}
}
Here's how I am attempting to use this new binding in 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="fill_parent"
android:layout_height="wrap_content"
android:paddingBottom="3dp"
android:paddingTop="3dp"
android:paddingLeft="5dp">
<LinearLayout
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:paddingBottom="3dp"
android:paddingTop="3dp"
android:paddingLeft="0dp"
local:MvxBind="Content CustomView">
</LinearLayout>
</LinearLayout>
Any my ViewModel looks like this:
public class CustomViewModel : MvxViewModel
{
public object CustomView { get; set; }
}
The custom binding has also been registered in Setup.cs as follows:
protected override void FillTargetFactories(IMvxTargetBindingFactoryRegistry registry)
{
registry.RegisterPropertyInfoBindingFactory(
typeof(MvxLinearLayoutContentTargetBinding),
typeof(LinearLayout), "Content");
base.FillTargetFactories(registry);
}
Yet with all this in place, I do not see my view.
MvvmCross already supports a primitive version of this. Although without DataTemplateSelector.
You can bind a collection of ViewModels to MvxLinearLayout.ItemsSource. You also need to remember to set a ItemTemplateId:
<MvxLinearLayout
...
local:MvxItemTemplateId="#layout/layout_to_repeat"
local:MvxBind="ItemsSource ViewModelCollection"
/>
This is however super inefficient as it does not recycle views etc. So if you need a variant that supports DataTemplateSelector, then use MvxRecyclerView instead.

OnNavigatedTo() method is not triggered in Xamarin.Forms

I have inherited my view model class from INavigateAware interface as below,
public class ViewModel : INavigationAware
{
public ViewModel()
{
}
public void OnNavigatedFrom(NavigationParameters parameters)
{
}
public void OnNavigatedTo(NavigationParameters parameters)
{
// some codes
}
}
And called that view model in the associated view(the page I have been navigated to)
public partial class Page1 : ContentPage
{
ViewModel viewModel;
public Page1()
{
InitializeComponent();
viewModel = new ViewModel();
this.Content = myview; //myview is my control like grid
}
}
Now my problem is when I navigate to this page(Page1), OnNavigateTo() method in ViewModel is not triggered. Please someone helps me how to make trigger OnNavigateTo() method.
Thanks in advance.
First thing first, check if you have AutowireViewModel parameter in your page class set to True.
Second, you should not assign view model yourself, prism will do that for you, when you call PushViewModel
Third there is well known limitation in prism:
https://github.com/PrismLibrary/Prism/issues/563
There is also workaround suggested:
Create interface:
public interface IPageNavigationAware
{
void OnAppearing();
void OnDisappearing();
}
Derive your ViewModel class from this interface.
In the Views code behind:
protected override void OnAppearing()
{
(BindingContext as IPageNavigationAware)?.OnAppearing();
}
protected override void OnDisappearing()
{
(BindingContext as IPageNavigationAware)?.OnDisappearing();
}
The problem with that is that OnAppearing/OnDissapparing are not reliable navigation methods and do not accept parameters, but rather page lifecycle methods. They do not indicate when a page has been navigated to or from. You can have instances where a parent page can be appearing at the same time as multiple child pages are appearing. This will be addressed when Xamarin provides a proper navigation API.

How to connect View and ViewModel using MVVMCross for Windows Phone Universal Platform?

I have a pcl dll (Xamarin) and all the viewmodels, models, services, interfaces, converters are on this project.
I am already using this core on Android and IOS and now will start the Windows Phone app.
My main question for now is, how do I make a viewmodel the datacontext for a view. For sample: LoginViewModel.cs (core) and LoginView.xaml...
I am using MVVMCross and The Windows Phone project is 8.1
Than you in advance.
Updating:
I tried this:
<views:MvxWindowsPage
x:Class="Tocalivros.WindowsPhone.Views.LoginView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Tocalivros.WindowsPhone.Views"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:views="using:Cirrious.MvvmCross.WindowsCommon.Views"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
public sealed partial class LoginView : MvxWindowsPage<LoginViewModel>
{
public LoginView()
{
this.InitializeComponent();
}
But now I get this error: "Severity Code Description Project File Line
Error CS0263 Partial declarations of 'LoginView' must not specify different base classes"
Based on samples, I tried a different aproach, instead of making the view of type view model, I create a ViewModel as a property and set it as DataContext for the view.
Like this:
public sealed partial class LoginView : MvxWindowsPage
{
public new LoginViewModel ViewModel
{
get { return (LoginViewModel)base.ViewModel; }
set { base.ViewModel = value; }
}
public LoginView()
{
this.InitializeComponent();
this.DataContext = ViewModel;
}
But this way the viewmodel wont be initiated with the required parameters... (IServices)
It makes me think the better aproach is the first:
public sealed partial class LoginView : MvxWindowsPage<LoginViewModel> but I get error...
You can do like with every other platform in MvvmCross.
public class LoginPage : MvxWindowsPage<LoginViewModel>
{
}
Inside your view you will see something like this:
<views:MvxWindowsPage
x:Class="somenamspace.Views.LoginPage">
<!-- Put your layout here -->
</views:MvxWindowsPage>
The datacontext is now done for that view.

My databinding fails, is the path wrong?

I'm trying to create a simple RSS news aggregator. But I've done something wrong with the binding.
The xaml:
<phone:LongListSelector
x:Name="MainLongListSelector"
Margin="0,0,-12,0"
ItemsSource="{Binding Path=News.List}"
SelectionChanged="MainLongListSelector_SelectionChanged">
The code behind:
(mainpage)
public MainPage()
{
InitializeComponent();
// Set the data context of the LongListSelector control
// to the sample data
DataContext = App.ViewModel;
(viewmodel)
public class NewsViewModel : BaseViewModel
{
public NewsRepository News { get; private set; }
}
The NewsRepository is just an model object holding the List<obj> List. Could anyone point me in the direction where thing goes sideways?
The app runs just fine but the single item in the List I've put in manually does not show up in the application :-(
I've set and breakpoint at the assignment of the DataContext to the the viewmodels content its there...
Both NewsViewModel and NewsRepository have to implement INotifyPropertyChanged.
It would be easier if you use ObservableCollection<obj> as a source of your binding instead of custom NewsRepository object.

Resources