I created an Android application with Xamarin and MvvmCross.
I want to bind some views (text, editing text, button) to my ViewModel. Nothing strange well so far. But my bindings don’t apply… When I use typed FindViewById, I don’t get the traced error but bindings doesn’t apply.
When I run the application, I have the following trace:
MvxBind:Error: Empty binding target passed to MvxTargetBindingFactoryRegistry
MvxBind:Warning: Failed to create target binding for binding for TextProperty
My override of OnCreate(Bundle bundle) void is :
SetContentView(Resource.Layout.Reference);
var referenceTextView = FindViewById(Resource.Id.referenceEditView); // untyped FindViewById
var siteTextView = FindViewById<TextView>(Resource.Id.siteTextView); // typed FindViewById<T>
//var goButton = FindViewById<Button>(Resource.Id.goButton);
var bindingsSet = this.CreateBindingSet<ReferenceView, ReferenceViewModel>();
bindingsSet.Bind(referenceTextView).To(vm => vm.Reference).Mode(MvxBindingMode.TwoWay);
bindingsSet.Bind(siteTextView).To(vm => vm.Site);
//bindingsSet.Bind(goButton).To(vm => vm.GoCommand);
bindingsSet.Apply();
base.OnCreate(bundle);
I’ve tried to do in the AXML :
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/siteTextView"
android:text="####"
local:MvxBind="Text Site"
android:gravity="center" />
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="#+id/referenceTextView"
android:hint="Numéro de dossier"
local:MvxBind="Text Reference" />
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Accéder"
android:id="#+id/goButton"
local:MvxBind="Click GoCommand" />
The getters and setters of my properties use RaiseAndSetIfChanged method :
private string _reference;
public string Reference
{
get { return _reference; }
set { this.RaiseAndSetIfChanged(ref _reference, value, () => Reference); }
}
I’ve the same LinkerPleaseInclude class than LinkerPleaseInclude original class.
My setup inherits from MvxAndroidSetup class
And on other ViewModels, the bindings are applied correctly.
You need to call base.OnCreate(bundle); before SetContentView as the ViewModel is located and attached inside of that call. Failing to do so will obviously give you the exact errors you see. Source will be null and will not bind to the target.
So you can either do:
base.OnCreate(bundle);
SetContentView(Resource.Layout.Reference);
And have all your bindings in your AXML. Or you can do the other approach setting the bindings behind the scenes:
base.OnCreate(bundle);
SetContentView(Resource.Layout.Reference);
var referenceTextView = FindViewById<TextView>(Resource.Id.referenceEditView);
var siteTextView = FindViewById<TextView>(Resource.Id.siteTextView);
var bset = this.CreateBindingSet<ReferenceView, ReferenceViewModel>();
bset.Bind(referenceTextView).To(vm => vm.Reference);
bset.Bind(siteTextView).To(vm => vm.Site);
bset.Apply();
Just make sure to call base.OnCreate to begin with.
The warnings
MvxBind:Error: 2,20 Empty binding target passed to MvxTargetBindingFactoryRegistry
MvxBind:Warning: 2,20 Failed to create target binding for binding for Text
are caused by var referenceTextView = FindViewById(Resource.Id.referenceEditView); resulting in referenceTextView to be of type View.
MvvmCross is searching for the default binding target property of the type TTarget when calling Bind<TTArget> without For(targetProperty). This is just a look up in a table like:
TTarget Property
----------------------
TextView Text
Button Click
... ...
In your case TTarget is View instead of TextView, because you pass it into bindingsSet.Bind(referenceTextView) wich is the implicit call of bindings.Bind<View>(btnNumber). View has no default binding target property. You have to set it explicitly like
bindings.Bind(btnNumber).For("Text")
or use the typed FindViewById<TextView>.
I dont think you need to bind twice, remove these lines:
var referenceTextView = FindViewById(Resource.Id.referenceEditView); // untyped FindViewById
var siteTextView = FindViewById<TextView>(Resource.Id.siteTextView); // typed FindViewById<T>
//var goButton = FindViewById<Button>(Resource.Id.goButton);
var bindingsSet = this.CreateBindingSet<ReferenceView, ReferenceViewModel>();
bindingsSet.Bind(referenceTextView).To(vm => vm.Reference).Mode(MvxBindingMode.TwoWay);
bindingsSet.Bind(siteTextView).To(vm => vm.Site);
//bindingsSet.Bind(goButton).To(vm => vm.GoCommand);
bindingsSet.Apply();
So your on create is just this:
SetContentView(Resource.Layout.Reference);
base.OnCreate(bundle);
And keep the bindings in the axml file.
Make sure you have this at the top of your xaml file:
xmlns:local="http://schemas.android.com/apk/res-auto"
Also if you are doing bindings in the cs file, the MvvmCross binding mode is TwoWay by default. So you dont need .Mode(MvxBindingMode.TwoWay);
Related
I made a Custom Layout to add a badge to a tab on Android. This badge is a TextView.
How do I bind this component?
It's possible?
My custom badge layout:
<LinearLayout
android:id="#+id/badgeCotainer"
android:layout_width="wrap_content"
android:layout_height="20dp"
android:layout_marginStart="90dp"
android:background="#drawable/notifications_background"
android:gravity="center"
android:layout_gravity="right"
android:minWidth="16dp">
<TextView
android:id="#+id/badge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAlignment="center"
android:textColor="#011f7a"
android:textSize="10sp"
android:text="0"
app:MvxBind="Text ContMaterial"/>
</LinearLayout>
My Activity: use MvxViewPagerFragmentInfo
private void InitTabs()
{
var viewPager = FindViewById<ViewPager>(Resource.Id.materiais_viewpager);
if (viewPager != null)
{
var fragments = new List<MvxViewPagerFragmentInfo>
{
new MvxViewPagerFragmentInfo("Requisição",
typeof(CadastraRequisicaoFragment), ViewModel),
new MvxViewPagerFragmentInfo("Materiais",
typeof(ListaMateriaisFragment), ViewModel),
};
viewPager.Adapter = new MvxCachingFragmentStatePagerAdapter(this, SupportFragmentManager, fragments);
}
var tabLayout = FindViewById<TabLayout>(Resource.Id.requisicao_tabs);
tabLayout.SetupWithViewPager(viewPager);
tabLayout.GetTabAt(1).SetCustomView(Resource.Layout.tab_header_badge);
}
On view Model I used raisedPropertyChanged()
Badge appears normally, just don't change value.
Tablayout
Out of the box there is nothing in mvvmcross to bind the content of a TabLayout when you provide a custom view. You could however try something custom using MvxFrameControl
Basically, replace your LinearLayout by the MvxFrameControl, then after
tabLayout.SetupWithViewPager(viewPager);
inflate your custom view using the BindingInflate method, and set a value to your MvxFrameControl's BindingContext, then set your custom tablayout view with the result that operation.
That might do the trick.
Another way to do it, is to listen to your ViewModel's "PropertyChanged" events in your Activity, and manually make changes to your tab layouts when it applies.
I have inherited some code that requires a change in how it works. The original way didn't have the flexibility now required.
The application is a form generator, and hence has to create the UI on demand. This is Xamarin native, not Xamarin forms.
A FrameLayout for each form question is being created programmatically, added to the view, then a fragment is being added to this FrameLayout. All this is happening AFTER OnCreateView once the UI has been loaded to show a progress circle.
After working through a bunch of exceptions, I have become stuck with the exception
Java.Lang.IllegalArgumentException: No view found for id 0x50 (unknown) for fragment UploadFragment{a31e878 #7 id=0x50 upload_80}
My guess is that the FrameLayout doesn't exist when the fragment is trying to be displayed.
The exception occurs after the OnCreate() method runs after OnCreateView() completes.
I have not been able to find any code precedent for adding FrameLayouts programmatically with Fragments.
CODE Snippet
frame = new FrameLayout(this.Context);
frame.LayoutParameters = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WrapContent, ViewGroup.LayoutParams.WrapContent);
upload = new Widgets.UploadFragment(control, binding, Inflater, a, xFormInstance);
MainFormLayout.AddView(frame);
frame.Id = control.id;
fragmentTx.Add(frame.Id, upload, $"upload_{control.id}");
fragmentTx.Commit();
Any advice would be greatly appreciated. Thanks.
Extended Explanation
It may be a bit much to put in everything it does, but will try and put in as much as I can.
The Hierarchy of the page is
Activity -> FormFragment -> UploadFragment
So the parent of the UploadFragment is also a fragment, not the Activity.
Upload Fragment
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android">
<RelativeLayout>
<LinearLayout>
<TextView/>
<ImageButton/>
</LinearLayout>
<ImageView/>
</RelativeLayout>
</LinearLayout>
CODE
public override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
}
public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
// Use this to return your custom view for this Fragment
_inflater = inflater;
v = _inflater.Inflate(Resource.Layout.BindImageInput, container, false);
SetUpload();
return v;
//return base.OnCreateView(inflater, container, savedInstanceState);
}
SetUpload() Sets the values of the label, the events for the buttons, and the image (if exists) to the imageview. It also deals with a few extra events to do with form event handling. Stopping SetUpload() from running still has the exception occur.
FormFragment
<RelativeLayout>
<TextView />
<View />
<ScrollView>
<LinearLayout />
</ScrollView>
</RelativeLayout>
CODE
public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
ShowLoading();
View v = inflater.Inflate(Resource.Layout.Form2, container, false);
MainFormLayout = v.FindViewById<LinearLayout>(Resource.Id.mainFormView);
MainScrollView = v.FindViewById<ScrollView>(Resource.Id.mainScrollView);
formBuilderWorker = new BackgroundWorker();
return v;
}
OnResume() Calls the method where formBuilderWorker.DoWork() exists
formBuilderWorker.DoWork += delegate
{
Form.LoadForm(null, this, FormInstance);
}
LoadForm() uses a Interface to tell the FormFragment to display a control. One of which is the UploadFragment.
public void AddControl(Controls control, int? sectionID)
{
///CODE REMOVED FOR OTHER CONTROL TYPES (they still use old codebase)
Bindings binding = XForm.GetBindingForControl(control, FormInstance);
try
{
// Create a new fragment and a transaction.
FragmentTransaction fragmentTx = this.FragmentManager.BeginTransaction();
FrameLayout frame = null;
Widgets.UploadFragment upload = null;
frame = new FrameLayout(this.Context);
frame.LayoutParameters = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WrapContent, ViewGroup.LayoutParams.WrapContent);
frame.Id = control.id;
upload = new Widgets.UploadFragment(control, binding, Inflater, a, xFormInstance);
MainFormLayout.AddView(frame);
ControlViews.Add(frame);
fragmentTx.Replace(frame.Id, upload, $"upload_{control.id}");
//fragmentTx.Show(upload);
fragmentTx.Commit();
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
}
This is cleaned code to remove as much irrelevant code as possible. The code shown is the path the code in question moves through.
I found the issue. Part of what I took out of the code above, was the Activity.RunOnUiThread() calls that add the frame to the main view. The issue was caused by Thread Timing. The UI thread was taking so long to add the frame to the view, that when the FragmentTransaction was trying to commit the changes, the frame still did not exist.
I've been looking at the Playground project on MvvmCross (https://github.com/MvvmCross/MvvmCross/tree/develop/Projects/Playground)
The way to have a fragment loaded into the tabs is to set the attribute the following way:
[MvxTabLayoutPresentation(TabLayoutResourceId = Resource.Id.tabs, ViewPagerResourceId = Resource.Id.viewpager, Title = "Tab 1", ActivityHostViewModelType = typeof(TabsRootViewModel))]
[MvxTabLayoutPresentation(TabLayoutResourceId = Resource.Id.tabs, ViewPagerResourceId = Resource.Id.viewpager, Title = "Tab 1", FragmentHostViewType = typeof(TabsRootBView))]
[Register(nameof(Tab1View))]
public class Tab1View : MvxFragment<Tab1ViewModel>
My questions is, besides the title that can be specified on the MvxTabLayoutPresentation, how can I add and icon to each one of the tabs?
That is not provided out-of-the-box and it's more about TabLayout customization.
You can achieve it like this.
Create a CustomView for your tab layout item, myCustomTab.axml:
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/txtTab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:textColor="#FFFFFF"
android:textSize="12"
android:textStyle="bold" />
Then in your view that has the TabLayout you configure it on the OnCreate / OnCreateView:
var tabLayout = view.FindViewById<TabLayout>(Resource.Id.tabs);
var customTab = inflater.Inflate(Resource.Layout.myCustomTab, null);
customTab.Text = "MyText";
// this sets the icon above the text
customTab.SetCompoundDrawablesWithIntrinsicBounds(0, Resource.Drawable.my_icon, 0, 0);
tabLayout.GetTabAt(0).SetCustomView(customTab);
Obviously you have to do this as many times as tab layout items you have.
Furthermore using this you can add any customization to your tab layout items.
Source (in java): https://mobikul.com/make-custom-tabs-icons-android/
HIH
I am trying to use the visibility converter from MvvmCross but for some reason the visibility of the view that I'm trying to edit doesn't change at all.
I am using fluent binding in order to bind the element:
var set = this.CreateBindingSet<NextStopFragment, NextStopViewModel>();
set.Bind(Header).For(v => v.Visibility).To(vm => vm.LayoutVisibility_0).WithConversion("Visibility");
set.Apply();
And this is the code from my viewmodel:
private bool _layoutVisibility_0 = false;
public bool LayoutVisibility_0
{
get { return _layoutVisibility_0; }
set { _layoutVisibility_0 = value; RaisePropertyChanged(() => LayoutVisibility_0); }
}
public override void Prepare(Parameter parameter)
{
LayoutVisibility_0 = true;
}
This is the layout of the view that I'm binding (please note that I am restricted to using fluent binding on this):
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/NextStopHeader"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="#drawable/border_bottom_gray"
android:paddingLeft="15dp"
android:paddingRight="15dp"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:visibility="gone"/>
For the moment I am only trying to make the LinearLayout visible from the start, but it doesn't work. The binding works (I think) because the property gets updated correctly every time I am trying to edit it, but even if the property is set to true, the view remains "gone".
Have I missed something? Do I need to add anything else? Please note that this is ALL the code that I'm using for editing the visibility.
When my activity starts and I initially load my ListView with items, I want to be able to set some of those items to checked however that doesn't seem to be working.
I'm using the simple_list_item_multiple_choice layout so the items are CheckedTextView.
Here's the adapter. Text displays properly however the Checked property doesn't seem to be doing anything visually despite it being set to true/false.
public class VehicleSubListAdapter : BaseAdapter<string>
{
// ...
public override View GetView(int position, View convertView, ViewGroup parent)
{
View view = convertView ?? _context.LayoutInflater.Inflate(Android.Resource.Layout.SimpleListItemMultipleChoice, parent, false);
var checkedTextView = view.FindViewById<CheckedTextView>(Android.Resource.Id.Text1);
checkedTextView.Text = _items[position].Name;
// This doesn't seem to do anything
checkedTextView.Checked = _items[position].Checked;
// I've even tried this with no change visually
//checkedTextView.Checked = true;
return view;
}
}
Here's the activity which can be closed and re-opened but needs to resume from where it left off (some items checked, some items not). That is why things are stored in a global static variable.
If the global list is empty, manually update the lists. Else, load from the global list. It successfully loads from the global list and successfully stores the Checked property of that list as modified in the item click event. However, the checked items aren't shown as checked when the activity loads.
Also I've tried putting the call to FillList() in both OnCreate and OnResume and neither have worked.
public class VehicleConditionSubActivity : ListActivity
{
private List<VehicleConditionItemDTO> _items;
private VehicleSubListAdapter _vehicleAdapter;
//...
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
SetTheme(Resource.Style.MyAppTheme);
SetContentView(Resource.Layout.VehicleConditionItem);
// Set up action bar
ActionBar.SetDisplayHomeAsUpEnabled(true);
// Set up list view
ListView.ChoiceMode = ChoiceMode.Multiple;
_items = new List<VehicleConditionItemDTO>();
_vehicleAdapter = new VehicleSubListAdapter(this, _items);
ListAdapter = _vehicleAdapter;
ListView.ItemClick += ListView_ItemClick;
//...
// Fill list view
FillList();
}
private void FillList()
{
AddFluidItems();
_vehicleAdapter.NotifyDataSetChanged();
}
private void AddFluidItems()
{
// I've checked that this is being called correctly
if (Global.Conditions.FluidLevels.Count > 0)
{
foreach (var item in Global.Conditions.FluidLevels)
{
_items.Add(item);
}
}
else
{
_items.Add(new VehicleConditionItemDTO("1", "Engine oil"));
// ...
foreach (var item in _items)
{
Global.Conditions.FluidLevels.Add(item);
}
}
}
// ...
private void ListView_ItemClick(object sender, AdapterView.ItemClickEventArgs e)
{
// Items already check/uncheck properly when tapped
// So I just update the global list items checked property
Global.Conditions.FluidLevels[e.Position].Checked = !Global.Conditions.FluidLevels[e.Position].Checked;
}
}
The layout for the activity is
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:minWidth="25px"
android:minHeight="25px"
android:background="#color/background">
<ListView
android:minWidth="25px"
android:minHeight="25px"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:id="#android:id/list" />
</LinearLayout>
Selecting Items Programmatically
Manually setting which items are ‘selected’ is done with the
SetItemChecked method (it can be called multiple times for multiple
selection)
Customizing A List View's Appearance
Note that SetItemChecked is on the ListView.
You can remove the code in GetView that sets Checked.
Then, after (or during the call to FillList) for each item that should be initially checked, call:
ListView.SetItemChecked (position, true);