I have used the following template in my Android application which has navigation drawer has a list of options such as Settings.
https://github.com/MvvmCross/MvvmCross-Samples/tree/master/XPlatformMenus
Source code could be downloaded from the following url
https://github.com/MvvmCross/MvvmCross-Samples
I wonder how could I able to make Settings page as a Dialog or CustomFragment which will look like similar to following image.
One approach that you could make use of is to create a custom implementation of Dialog. Following the XPlatformMenus sample you linked to, you could implement something as follows:
Generic Custom Dialog
This class inherits android Dialog control, and can be used with any XML/AXML layout you want. You could tightly couple it to a particular ViewModel/Layout or you can make it handle a generic ViewModel type. Here is an example of the generic type:
public class CustomDialog : Dialog, IMvxBindingContextOwner
{
public CustomDialog(Context context, int layout, IMvxViewModel viewModel)
: this(context, Resource.Style.CustomDialog)
{
this.BindingContext = new MvxAndroidBindingContext(context, (context as IMvxLayoutInflaterHolder));
ViewModel = viewModel;
Init(layout);
}
public CustomDialog(Context context, int themeResId)
: base(context, themeResId)
{
}
protected CustomDialog(IntPtr javaReference, JniHandleOwnership transfer)
: base(javaReference, transfer)
{
}
protected CustomDialog(Context context, bool cancelable, IDialogInterfaceOnCancelListener cancelListener)
: base(context, cancelable, cancelListener)
{
}
protected CustomDialog(Context context, bool cancelable, EventHandler cancelHandler)
: base(context, cancelable, cancelHandler)
{
}
private void Init(int layout)
{
SetContentView(layout);
}
public override void SetContentView(int layoutResID)
{
var view = this.BindingInflate(layoutResID, null);
base.SetContentView(view);
}
public IMvxBindingContext BindingContext { get; set; }
public object DataContext
{
get { return this.BindingContext.DataContext; }
set { this.BindingContext.DataContext = value; }
}
public IMvxViewModel ViewModel
{
get { return this.DataContext as IMvxViewModel; }
set { this.DataContext = value; }
}
}
XML layout for modal:
<?xml version="1.0" encoding="utf-8" ?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:local="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#color/colorPrimary">
<Button
android:id="#+id/btn_option"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Show"
local:MvxBind="Click ShowSettingsCommand"/>
<Button
android:id="#+id/btn_close"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="#id/btn_option"
android:text="CLOSE"
local:MvxBind="Click ShowCloseCommand"/>
</RelativeLayout>
CustomDialog style:
<resources>
<style name="CustomDialog">
<item name="android:windowIsFloating">true</item>
<item name="android:windowNoTitle">true</item>
</style>
</resources>
Custom Presenter
Create a custom presenter to handle the navigation to show/hide the dialog:
public class CustomPresenter : MvxFragmentsPresenter
{
protected IMvxViewModelLoader MvxViewModelLoader => Mvx.Resolve<IMvxViewModelLoader>();
CustomDialog _modal;
public CustomPresenter(IEnumerable<Assembly> AndroidViewAssemblies) : base(AndroidViewAssemblies)
{
}
protected override void ShowActivity(MvxViewModelRequest request, MvxViewModelRequest fragmentRequest = null)
{
if (!Intercept(request))
base.ShowActivity(request, fragmentRequest);
}
protected override void ShowFragment(MvxViewModelRequest request)
{
if (!Intercept(request))
base.ShowFragment(request);
}
private bool Intercept(MvxViewModelRequest request)
{
if (request.ViewModelType == typeof(ThirdViewModel))
{
var activity = Mvx.Resolve<IMvxAndroidCurrentTopActivity>().Activity;
var viewModel = MvxViewModelLoader.LoadViewModel(request, null) as ThirdViewModel;
_modal = new CustomDialog(activity, Resource.Layout.modal_popup, viewModel);
_modal.Show();
return true;
}
if (_modal != null)
{
_modal.Dismiss();
_modal = null;
}
return false;
}
}
Register your custom presenter in the setup class:
protected override IMvxAndroidViewPresenter CreateViewPresenter()
{
var mvxFragmentsPresenter = new CustomPresenter(AndroidViewAssemblies);
Mvx.RegisterSingleton<IMvxAndroidViewPresenter>(mvxFragmentsPresenter);
return mvxFragmentsPresenter;
}
ViewModel
public class ThirdViewModel : BaseViewModel
{
private MvxCommand _showSettingsCommand;
public MvxCommand ShowSettingsCommand =>
_showSettingsCommand ?? (_showSettingsCommand = new MvxCommand(() => ShowViewModel<HomeViewModel>()));
private MvxCommand _showCloseCommand;
public MvxCommand ShowCloseCommand =>
_showCloseCommand ?? (_showCloseCommand = new MvxCommand(() => ShowViewModel<SettingsViewModel>()));
}
Related
Binding command to a Button - yields no effect (Xamarin, MVVM):
notes:
Pressing the Button and nothing happens: no CanExecute check occur.
Binding a Button in a ContentPage which is Part of a Tabbed-Template
functionality check and the rest of related MVVM binding works well: Defined a clicked-event and manually triggered the command from the code behind.
//Could someone see the reason?// editted
editted, new:
What would be a good practice when CanExecute relies on fields of a compound data type that are updated independently ? (*can take of the command parameter which is the compound data type, which is accessible to the command directly through the VM).
xaml for the View:
<ContentPage.Content>
<StackLayout>
<Entry Placeholder="Notes"/>
<Entry x:Name="courseIDEntry"
Text="{Binding CourseID, Mode=TwoWay}"
IsReadOnly="{Binding !ExistUnit}"
Placeholder="CourseID *"/>
<Entry x:Name="unitIDEntry"
Text="{Binding UnitID, Mode=TwoWay}"
IsReadOnly="{Binding !ExistUnit}"
Placeholder="UnitID *"/>enter code here
<Label Text="* Fields are mandatory"/>
<Button x:Name="AddSave"
Text="{Binding CommandText}"
Command="{Binding AddSaveCMD}"
CommandParameter="{Binding EdittedUnit}"/>
<!--Clicked="AddSave_Clicked"/>-->
</StackLayout>
</ContentPage.Content>enter code here
c# code behind for the view (*including the Button-Clicked check for)
public partial class EditUnitPage : ContentPage
{
EditUnitViewModel editUVM;
public EditUnitPage()
{
InitializeComponent();
editUVM = new EditUnitViewModel();
BindingContext = editUVM;
}
public EditUnitPage(Unit6 unitSelected) : this()
{
if (unitSelected != null)
{
editUVM.EdittedUnit = unitSelected;
editUVM.ExistUnit = true;
}
}
protected override void OnAppearing()
{
base.OnAppearing();
}
//private void AddSave_Clicked(object sender, EventArgs e)
//{
// if (editUVM.AddSaveCMD.CanExecute(editUVM.EdittedUnit))
// {
// editUVM.AddSaveCMD.Execute(null);
// }
//}
}
C# MyCommand (newbie. using ICommand and not the Command Class)
public class AddSaveUnitCommand : ICommand
{
public EditUnitViewModel EditUVM { get; set; }
public event EventHandler CanExecuteChanged;
public AddSaveUnitCommand(EditUnitViewModel euvm)
{
EditUVM = euvm;
}
public bool CanExecute(object parameter)
{
var editted = parameter as Unit6;
if (editted != null )
{
if (!string.IsNullOrEmpty(editted.CourseID) || !string.IsNullOrEmpty(editted.UnitID))
return true;
}
return false;
}
public void Execute(object parameterf)
{
EditUVM.AddSaveUnitAsync();
}
}
c# for VM (BaseViewModel implements INotify)
public class EditUnitViewModel : BaseViewModel
{
public AddSaveUnitCommand AddSaveCMD { get; set; }
private Unit6 edittedUnit;
public Unit6 EdittedUnit
{
get { return edittedUnit; }
set { edittedUnit = value; OnPropertyChanged(); }
}
private bool existUnit;
public bool ExistUnit
{
get { return existUnit; }
set
{
existUnit = value;
//OnPropertyChanged();
}
}
public string CommandText
{
get { return ExistUnit? "Save": "Add"; }
}
public string CourseID
{
get { return EdittedUnit.CourseID; }
set { EdittedUnit.CourseID = value; OnPropertyChanged(); }
}
public string UnitID
{
get { return EdittedUnit.UnitID; }
set { EdittedUnit.UnitID = value; OnPropertyChanged(); }
}
public EditUnitViewModel()
{
EdittedUnit = new Unit6();
AddSaveCMD = new AddSaveUnitCommand(this);
}
public async void AddSaveUnitAsync()
{
var curPage = App.Current.MainPage;
try
{
switch (ExistUnit)
{
case false: //insert new unit to the DB
EdittedUnit.UnitKey = ""; //Todo: look for more elegant of assigning auto value to property
Unit6.Insert(EdittedUnit);
break;
case true: //update details on existing unit
EdittedUnit.UnitKey = ""; //Todo: look for more elegant of assigning auto value to property
Unit6.Update(EdittedUnit);
break;
}
await curPage.DisplayAlert("Success", "Unit was succesffuly updateded", "OK");
}
catch
{
await curPage.DisplayAlert("Error", "Unit was not updated", "OK");
}
finally
{
EdittedUnit = null;
await curPage.Navigation.PushAsync(new MyTabbedPage());
}
}
}
xaml for the TabbedPage:
<TabbedPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:views="clr-namespace:P205.Views"
x:Class="P205.Views.MyTabbedPage">
<views:UnitsPage Title="Units" />
<views:EditUnitPage x:Name="editOrAddUnit" Title="Edit U"/>
<views:DBChangesPage Title="Edit DB"/>
<views:CoursesPage Title="Course"/>
<ContentPage Padding="10">
</ContentPage>
When your ViewModel defines a property of type ICommand, the ViewModel must also contain or reference a class that implements the ICommand interface. This class must contain or reference the Execute and CanExecute methods, and fire the CanExecuteChanged event whenever the CanExecute method might return a different value.
So you could try change like below:
public class AddSaveUnitCommand : ICommand
{
public EditUnitViewModel EditUVM { get; set; }
public event EventHandler CanExecuteChanged;
public AddSaveUnitCommand(EditUnitViewModel euvm)
{
EditUVM = euvm;
}
public bool CanExecute(object parameter)
{
var editted = parameter as Unit6;
if (editted != null )
{
if (!string.IsNullOrEmpty(editted.CourseID) || !string.IsNullOrEmpty(editted.UnitID))
return true;
}
return false;
}
public void Execute(object parameterf)
{
EditUVM.AddSaveUnitAsync();
CanExecuteChanged?.Invoke(this, EventArgs.Empty); //add this line.
}
}
I have been trying to bind my ListView to my View model. The view model successfully retrieves 5 records from the database and the Listview seems to display 5 blank rows, however it is not showing binding for each field within each row.
I have spent a couple of days searching internet but I don't seem to be doing anything different. I was using master detail pages so I thought that it may be the issue so I set my Events page as first navigation page without master/detail scenario but to no avail. Please note that I am using Portable Ninject for my dependencies/IoC.
My App.Xamal.cs is is as follows:
public App (params INinjectModule[] platformModules)
{
InitializeComponent();
var eventsPage = new NavigationPage(new EventsPage());
//Register core services
Kernel = new StandardKernel(new MyAppCoreModule(), new MyAppNavModule(eventsPage.Navigation));
//Register platform specific services
Kernel.Load(platformModules);
//Get the MainViewModel from the IoC
eventsPage.BindingContext = Kernel.Get<EventsViewModel>();
((BaseViewModel)(eventsPage.BindingContext)).Init();
MainPage = eventsPage;
}
My EventsPage.Xaml is provided below:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MyApp.Views.EventsPage"
Title="Events">
<ContentPage.Content>
<ListView x:Name="Events" ItemsSource="{Binding Events}" >
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<Label Text="{Binding EventID}" BackgroundColor="Red" TextColor="White"
FontAttributes="Bold" />
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</ContentPage.Content>
</ContentPage>
My EventsPage.xaml.cs is provided below:
namespace MyApp.Views
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class EventsPage : ContentPage, IBaseViewFor<EventsViewModel>
{
public EventsPage ()
{
InitializeComponent ();
}
EventsViewModel _vm;
public EventsViewModel ViewModel
{
get => _vm;
set
{
_vm = value;
BindingContext = _vm;
}
}
}
}
My EventsViewModel is as follows, it successfully retrieves 5 records and OnPropertyChanged is fired for Events property:
namespace MyApp.ViewModels
{
public class EventsViewModel : BaseViewModel, IBaseViewModel
{
ObservableCollection<Event> _events;
readonly IEventDataService _eventDataService;
public ObservableCollection<Event> Events
{
get { return _events; }
set
{
_events = value;
OnPropertyChanged();
}
}
public EventsViewModel(INavService navService, IEventDataService eventDataService) : base(navService)
{
_eventDataService = eventDataService;
Events = new ObservableCollection<Event>();
}
public override async Task Init()
{
LoadEntries();
}
async void LoadEntries()
{
try
{
var events = await _eventDataService.GetEventsAsync();
Events = new ObservableCollection<Event>(events);
}
finally
{
}
}
}
}
My BaseViewModel is as follows:
namespace MyApp.ViewModels
{
public abstract class BaseViewModel : INotifyPropertyChanged
{
protected INavService NavService { get; private set; }
protected BaseViewModel(INavService navService)
{
NavService = navService;
}
bool _isBusy;
public bool IsBusy
{
get
{
return _isBusy;
}
set
{
_isBusy = value;
OnPropertyChanged();
OnIsBusyChanged();
}
}
protected virtual void OnIsBusyChanged()
{
}
public abstract Task Init();
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
// Secod BaseViewModel abstract base class with a generic type that will be used to pass strongly typed parameters to the Init method
public abstract class BaseViewModel<TParameter> : BaseViewModel
{
protected BaseViewModel(INavService navService) : base(navService)
{
}
public override async Task Init()
{
await Init(default(TParameter));
}
public abstract Task Init(TParameter parameter);
}
}
IBaseViewModel is just a blank interface:
public interface IBaseViewModel
{
}
IBaseViewFor is given below:
namespace MyApp.ViewModels
{
public interface IBaseViewFor
{
}
public interface IBaseViewFor<T> : IBaseViewFor where T : IBaseViewModel
{
T ViewModel { get; set; }
}
}
My Event model is as follows:
namespace MyApp.Models
{
public class Event
{
public int EventID;
}
}
Finally, the image of the output, as you can see that 5 rows are created with red background but EventID is not binding in each row. I have checked the data and EventID is returned. I have even tried to manually add records into Events list but to no avail, see the manual code and image below:
async void LoadEntries()
{
try
{
Events.Add((new Event() { EventID = 1 }));
Events.Add((new Event() { EventID = 2 }));
Events.Add((new Event() { EventID = 3 }));
Events.Add((new Event() { EventID = 4 }));
Events.Add((new Event() { EventID = 5 }));
}
finally
{
}
}
I have spent a lot of time on it but unable to find a reason for this anomaly, can someone please cast a fresh eye and provide help!?
You can only bind to public properties - ie, you need a getter
public class Event
{
public int EventID { get; set; }
}
Let's say we have MvvmCross 6.0.1 native app with one Android Activity containing BottomNavigationView implemented as in this blog post by James Montemagno but without navigating and replacing fragments.
What I would like to do is to bind BottomNavigationView items to MvxCommands (or MvxAsyncCommands) in ViewModel in order to navigate between several ViewModels.
What kind of architecture should I apply to achieve this? Is my approach correct or am I doing something against MVVM pattern and MvvmCross possibilities?
Full working example with several additions can be found here on github.
At the moment I have (scaffolded with MvxScaffolding).
MainContainerActivity and corresponding MainContainerViewModel - here I would like to store commands to navigate between view models
MainFragment and corresponding MainViewModel - this is the first fragment/view model
SettingsFragment and corresponding SettingsViewModel - I would like to navigate to it from MainViewModel and vice versa
FavoritesFragment and corresponding FavoritesViewModel
The main activity is as follows:
using Android.App;
using Android.OS;
using Android.Views;
using PushNotifTest.Core.ViewModels.Main;
using Microsoft.AppCenter;
using Microsoft.AppCenter.Analytics;
using Microsoft.AppCenter.Crashes;
using Microsoft.AppCenter.Push;
using Android.Graphics.Drawables;
using Android.Support.Design.Widget;
using MvvmCross.Binding.BindingContext;
using System;
using System.Windows.Input;
namespace PushNotifTest.Droid.Views.Main
{
[Activity(
Theme = "#style/AppTheme",
WindowSoftInputMode = SoftInput.AdjustResize | SoftInput.StateHidden)]
public class MainContainerActivity : BaseActivity<MainContainerViewModel>
{
protected override int ActivityLayoutId => Resource.Layout.activity_main_container;
BottomNavigationView bottomNavigation;
public ICommand GoToSettingsCommand { get; set; }
public ICommand GoToFavoritesCommand { get; set; }
public ICommand GoToHomeCommand { get; set; }
protected override void OnCreate(Bundle bundle)
{
base.OnCreate();
AddBottomNavigation();
}
private void AddBottomNavigation()
{
bottomNavigation = (BottomNavigationView)FindViewById(Resource.Id.bottom_navigation);
if (bottomNavigation != null)
{
bottomNavigation.NavigationItemSelected += BottomNavigation_NavigationItemSelected;
// trying to bind command to view model property
var set = this.CreateBindingSet<MainContainerActivity, MainContainerViewModel>();
set.Bind(this).For(v => v.GoToSettingsCommand).To(vm => vm.NavigateToSettingsCommand);
set.Bind(this).For(v => v.GoToHomeCommand).To(vm => vm.NavigateToHomeCommand);
set.Bind(this).For(v => v.GoToFavoritesCommand).To(vm => vm.NavigateToFavoritesCommand);
set.Apply();
}
else
{
System.Diagnostics.Debug.WriteLine("Bottom navigation menu is null");
}
}
private void BottomNavigation_NavigationItemSelected(object sender, BottomNavigationView.NavigationItemSelectedEventArgs e)
{
try
{
System.Diagnostics.Debug.WriteLine($"Bottom navigation menu is selected: {e.Item.ItemId}");
if (e.Item.ItemId == Resource.Id.menu_settings)
if (GoToSettingsCommand != null && GoToSettingsCommand.CanExecute(null))
GoToSettingsCommand.Execute(null);
if (e.Item.ItemId == Resource.Id.menu_list)
if (GoToFavoritesCommand != null && GoToFavoritesCommand.CanExecute(null))
GoToFavoritesCommand.Execute(null);
if (e.Item.ItemId == Resource.Id.menu_home)
if (GoToHomeCommand != null && GoToHomeCommand.CanExecute(null))
GoToHomeCommand.Execute(null);
}
catch (Exception exception)
{
System.Diagnostics.Debug.WriteLine($"Exception: {exception.Message}");
Crashes.TrackError(exception);
}
}
}
}
The bottom navigation elements are:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="#+id/menu_home"
android:enabled="true"
android:icon="#drawable/ic_history"
android:title="#string/tab1_title"
app:showAsAction="ifRoom" />
<item
android:id="#+id/menu_list"
android:enabled="true"
android:icon="#drawable/ic_list"
android:title="#string/tab2_title"
app:showAsAction="ifRoom" />
<item
android:id="#+id/menu_settings"
android:enabled="true"
android:icon="#drawable/ic_settings"
android:title="#string/tab3_title"
app:showAsAction="ifRoom" />
</menu>
And the commands in view model are just:
public IMvxAsyncCommand NavigateToSettingsCommand => new MvxAsyncCommand(async () => await _navigationService.Navigate<SettingsViewModel>());
public IMvxAsyncCommand NavigateToFavoritesCommand => new MvxAsyncCommand(async () => await _navigationService.Navigate<FavoritesViewModel>());
public IMvxAsyncCommand NavigateToHomeCommand => new MvxAsyncCommand(async () => await _navigationService.Navigate<MainViewModel>());
Instead of using Fluent Binding, you could create a targeted binding for the BottomNavigationView and handle the navigation in your MainViewModel. Use Swiss binding in your XML.
TargetBinding Class:
public class MvxBottomNavigationItemChangedBinding : MvxAndroidTargetBinding
{
readonly BottomNavigationView _bottomNav;
IMvxCommand _command;
public override MvxBindingMode DefaultMode => MvxBindingMode.TwoWay;
public override Type TargetType => typeof(MvxCommand);
public MvxBottomNavigationItemChangedBinding(BottomNavigationView bottomNav) : base(bottomNav)
{
_bottomNav = bottomNav;
_bottomNav.NavigationItemSelected += OnNavigationItemSelected;
}
public override void SetValue(object value)
{
_command = (IMvxCommand)value;
}
protected override void SetValueImpl(object target, object value)
{
}
void OnNavigationItemSelected(object sender, BottomNavigationView.NavigationItemSelectedEventArgs e)
{
if (_command != null)
_command.Execute(e.Item.TitleCondensedFormatted.ToString());
}
protected override void Dispose(bool isDisposing)
{
if (isDisposing)
_bottomNav.NavigationItemSelected -= OnNavigationItemSelected;
base.Dispose(isDisposing);
}
}
Setup.cs :
protected override void FillTargetFactories(IMvxTargetBindingFactoryRegistry registry)
{
MvxAppCompatSetupHelper.FillTargetFactories(registry);
base.FillTargetFactories(registry);
registry.RegisterCustomBindingFactory<BottomNavigationView>("BottomNavigationSelectedBindingKey",
view => new MvxBottomNavigationItemChangedBinding(view));
}
BottomNavigationView XML :
Note the target binding key that we added in Setup.cs is used when binding.
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="#+id/navigation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#color/white"
app:labelVisibilityMode="labeled"
app:menu="#menu/bottom_nav_menu"
app:elevation="10dp"
local:MvxBind="BottomNavigationSelectedBindingKey BottomNavigationItemSelectedCommand"/>
MainViewModel :
public class MainViewModel : BaseViewModel
{
public IMvxCommand<string> BottomNavigationItemSelectedCommand { get; private set; }
List<TabViewModel> _tabs;
public List<TabViewModel> Tabs
{
get => _tabs;
set => SetProperty(ref _tabs, value);
}
public MainViewModel(IMvxNavigationService navigationService) : base(navigationService)
{
//these are for android - start
BottomNavigationItemSelectedCommand = new MvxCommand<string>(BottomNavigationItemSelected);
var tabs = new List<TabViewModel>
{
Mvx.IoCProvider.IoCConstruct<FirstViewModel>(),
Mvx.IoCProvider.IoCConstruct<SecondViewModel>(),
Mvx.IoCProvider.IoCConstruct<ThirdViewModel>()
};
Tabs = tabs;
//end
}
// Android-only, not used on iOS
private void BottomNavigationItemSelected(string tabId)
{
if (tabId == null)
{
return;
}
foreach (var item in Tabs)
{
if (tabId == item.TabId)
{
_navigationService.Navigate(item);
break;
}
}
}
}
TabViewModel :
public class TabViewModel : BaseViewModel
{
public string TabName { get; protected set; }
public string TabId { get; protected set; }
public TabViewModel(IMvxNavigationService navigationService) : base(navigationService)
{
}
}
BottomNavigation Elements:
Add "android:titleCondensed", which will be used as the Id.
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="#+id/menu_home"
android:enabled="true"
android:icon="#drawable/ic_history"
android:title="#string/tab1_title"
android:titleCondensed ="tab_first"
app:showAsAction="ifRoom" />
<item
android:id="#+id/menu_list"
android:enabled="true"
android:icon="#drawable/ic_list"
android:title="#string/tab2_title"
android:titleCondensed ="tab_second"
app:showAsAction="ifRoom" />
<item
android:id="#+id/menu_settings"
android:enabled="true"
android:icon="#drawable/ic_settings"
android:title="#string/tab3_title"
android:titleCondensed ="tab_third"
app:showAsAction="ifRoom" />
</menu>
ViewModel Examples:
public class FirstViewModel : TabViewModel
{
public FirstViewModel(IMvxNavigationService navigationService) : base(navigationService)
{
TabId = "tab_first";
}
}
public class SecondViewModel : TabViewModel
{
public SecondViewModel(IMvxNavigationService navigationService) : base(navigationService)
{
TabId = "tab_second";
}
}
Hope this helps someone else who comes into this later on ! :)
How to display image with text ( like name, price.. etc )
Below code only display images with no text.
--- UI :
<?xml version="1.0" encoding="utf-8"?>
<GridView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/gridview"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:columnWidth="230dp"
android:numColumns="2"
android:verticalSpacing="10dp"
android:horizontalSpacing="10dp"
android:background="#ffffff"
android:stretchMode="columnWidth"
android:gravity="center" />
-- Code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Android.App;
using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Views;
using Android.Widget;
namespace ModSpforce
{
class ImageAdapter : BaseAdapter
{
Context context;
public ImageAdapter(Context c)
{
context = c;
}
public override int Count
{
get { return thumbIds.Length; }
}
public override Java.Lang.Object GetItem(int position)
{
return null;
}
public override long GetItemId(int position)
{
return 0;
}
// create a new ImageView for each item referenced by the Adapter
public override View GetView(int position, View convertView, ViewGroup parent)
{
ImageView imageView;
if (convertView == null)
{ // if it's not recycled, initialize some attributes
imageView = new ImageView(context);
imageView.LayoutParameters = new GridView.LayoutParams(200, 200);
imageView.SetScaleType(ImageView.ScaleType.CenterCrop);
imageView.SetPadding(3, 3, 3, 3);
}
else
{
imageView = (ImageView)convertView;
}
imageView.SetImageResource(thumbIds[position]);
return imageView;
}
// references to our images
int[] thumbIds = {
Resource.Drawable.sample_2, Resource.Drawable.sample_3,
Resource.Drawable.sample_4, Resource.Drawable.sample_5,
Resource.Drawable.sample_6, Resource.Drawable.sample_7,
Resource.Drawable.sample_0, Resource.Drawable.sample_1,
Resource.Drawable.sample_2, Resource.Drawable.sample_3,
Resource.Drawable.sample_4, Resource.Drawable.sample_5,
Resource.Drawable.sample_6, Resource.Drawable.sample_7,
Resource.Drawable.sample_0, Resource.Drawable.sample_1,
Resource.Drawable.sample_2, Resource.Drawable.sample_3,
Resource.Drawable.sample_4, Resource.Drawable.sample_5,
Resource.Drawable.sample_6, Resource.Drawable.sample_7
};
}
}
You can inflate your GridView's cell in the GetView method of your adapter, so you can simply design your item's template in xml.
For example:
Code behind of your GridView:
public ObservableCollection<MyItemModel> items = new ObservableCollection<MyItemModel>();
public MyGridViewAdapter adapter;
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
SetContentView(Resource.Layout.Main);
//add your items here.
for (int i = 0; i < 50; i++)
{
items.Add(new MyItemModel { ImageSource = Resource.Drawable.Pika, Name = "Name " + i });
}
adapter = new MyGridViewAdapter(this, items);
GridView gv = FindViewById<GridView>(Resource.Id.gridview);
gv.Adapter = adapter;
}
MyItemModel is for image resource and name of this image, like this:
public class MyItemModel
{
public string Name { get; set; }
public int ImageSource { get; set; }
}
And MyGridViewAdapter is like this:
public class MyGridViewAdapter : BaseAdapter<MyItemModel>
{
private ObservableCollection<MyItemModel> items;
private Activity context;
public MyGridViewAdapter(Activity context, ObservableCollection<MyItemModel> items)
{
this.items = items;
this.context = context;
}
public override MyItemModel this[int position]
{
get
{
return items[position];
}
}
public override int Count
{
get
{
return items.Count;
}
}
public override long GetItemId(int position)
{
return position;
}
public override View GetView(int position, View convertView, ViewGroup parent)
{
View view = convertView;
if (view == null)
{
view = context.LayoutInflater.Inflate(Resource.Layout.MyGridViewCell, null);
}
var image = view.FindViewById<ImageView>(Resource.Id.image);
image.SetImageResource(items[position].ImageSource);
var name = view.FindViewById<TextView>(Resource.Id.name);
name.Text = items[position].Name;
return view;
}
}
Finally the layout of MyGridViewCell is like this:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView android:id="#+id/image"
android:layout_height="200dp"
android:layout_width="200dp" />
<TextView android:id="#+id/name"
android:layout_height="wrap_content"
android:layout_width="200dp"
android:textColor="#android:color/holo_blue_light" />
</LinearLayout>
I have created a custom control (CustomCard) which is a subclass of the CardView control. I would like to use this control within my project in different places.
For example, I may place the CustomCard within an xml layout manually, or I may want the CustomCard to be an item in an MvxListView. The key is that I would like to re-use the code as much as possible and benefit from having control over the CustomCard class.
When the CustomCard is instantiated, I am inflating it's layout using the standard layout inflater, see code:
using System;
using Android.Animation;
using Android.Content;
using Android.Support.V7.Widget;
using Android.Util;
using Android.Views;
using Android.Widget;
public class Card : CardView
{
private readonly Context _context;
public Card(Context context)
: base(context)
{
_context = context;
Init();
}
public Card(Context context, IAttributeSet attrs)
: base(context, attrs)
{
_context = context;
Init();
}
private void Init()
{
var inflater = (LayoutInflater) _context.GetSystemService(Context.LayoutInflaterService);
CardView = inflater.Inflate(Resource.Layout.base_card, this);
}
}
Within the layout base_card.xml, I have some elements that I would like to bind using MVVMCross, for example,
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:local="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#android:color/white">
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:local="http://schemas.android.com/apk/res-auto"
android:id="#+id/basecard_title"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- Title Text-->
<TextView
android:id="#+id/tv_basecard_header_title"
style="#style/card.title"
android:text="title text"
local:MvxBind="Text Title"
/>
<!-- ImageView -->
<MvxImageView
android:id="#+id/ib_basecard_header_button_expand"
style="#style/card.image"
local:MvxBind="Bitmap ImageBytes,Converter=InMemoryImage"/>
</RelativeLayout>
</FrameLayout>
My actual base_card layout is much more complex.
If I try to use my CustomCard within another XML Layout, none of the binding takes place. I think this is because I am using the standard layout inflater to inflate my base_card within my CustomCard rather than BindingInflate() but I can't be sure.
I have searched on SO and through the forums but I can't find any references to anyone using a custom control that inflates it's own view when instantiated with MVVMCross binding.
Has anyone done it, or am I trying to do something that isn't possible?
I ran into similar issue with CardView control. Since CardView directly inherits from FrameLayout I decided to use implementation almost identical to MvxFrameControl (Thanks Stuart for pointing out MvxFrameControl sample):
public class MvxCardView : CardView, IMvxBindingContextOwner
{
private object _cachedDataContext;
private bool _isAttachedToWindow;
private readonly int _templateId;
private readonly IMvxAndroidBindingContext _bindingContext;
public MvxCardView(Context context, IAttributeSet attrs)
: this(MvxAttributeHelpers.ReadTemplateId(context, attrs), context, attrs)
{
}
public MvxCardView(int templateId, Context context, IAttributeSet attrs)
: base(context, attrs)
{
_templateId = templateId;
if (!(context is IMvxLayoutInflater))
{
throw Mvx.Exception("The owning Context for a MvxCardView must implement LayoutInflater");
}
_bindingContext = new MvxAndroidBindingContext(context, (IMvxLayoutInflater)context);
this.DelayBind(() =>
{
if (Content == null && _templateId != 0)
{
Mvx.Trace("DataContext is {0}", DataContext == null ? "Null" : DataContext.ToString());
Content = _bindingContext.BindingInflate(_templateId, this);
}
});
}
protected MvxCardView(IntPtr javaReference, JniHandleOwnership transfer)
: base(javaReference, transfer)
{
}
protected IMvxAndroidBindingContext AndroidBindingContext
{
get { return _bindingContext; }
}
public IMvxBindingContext BindingContext
{
get { return _bindingContext; }
set { throw new NotImplementedException("BindingContext is readonly in the list item"); }
}
protected View Content { get; set; }
protected override void Dispose(bool disposing)
{
if (disposing)
{
this.ClearAllBindings();
_cachedDataContext = null;
}
base.Dispose(disposing);
}
protected override void OnAttachedToWindow()
{
base.OnAttachedToWindow();
_isAttachedToWindow = true;
if (_cachedDataContext != null
&& DataContext == null)
{
DataContext = _cachedDataContext;
}
}
protected override void OnDetachedFromWindow()
{
_cachedDataContext = DataContext;
DataContext = null;
base.OnDetachedFromWindow();
_isAttachedToWindow = false;
}
[MvxSetToNullAfterBinding]
public object DataContext
{
get { return _bindingContext.DataContext; }
set
{
if (_isAttachedToWindow)
{
_bindingContext.DataContext = value;
}
else
{
_cachedDataContext = value;
if (_bindingContext.DataContext != null)
{
_bindingContext.DataContext = null;
}
}
}
}
}
Usage:
<YourNamespace.MvxCardView
android:layout_width="match_parent"
android:layout_height="match_parent"
local:MvxTemplate="#layout/base_card"
local:MvxBind="DataContext ." />
Note: Using custom implementation also solved my problem with binding click command to CardView control using local:MvxBind="Click MyCommand", which wasn't working until subclassing CardView.