Is it possible to bind a property of a view model to the title of a segment in UISegmentedControl?
I'm aware of the SetTitle() method, but not sure if it's possible to bind to this in MvvmCross.
Building off of Kiliman's answer to a similar question.
Follow the first 2 steps from that answer. Then create the following custom binding builder.
public class MyTouchBindingBuilder : MvxTouchBindingBuilder
{
protected override void FillTargetFactories(IMvxTargetBindingFactoryRegistry registry)
{
base.FillTargetFactories (registry);
registry.RegisterCustomBindingFactory<UISegmentedControl> ("Title", segmentTitle => new MvxSegmentTitleTargetBinding (segmentTitle));
}
}
And the following custom target binding.
public class MvxSegmentTitleTargetBinding : MvxConvertingTargetBinding
{
public MvxSegmentTitleTargetBinding(object target) : base(target)
{
}
public override Type TargetType
{
get {return typeof(MyViewModel);}
}
protected override void SetValueImpl(object target, object value)
{
var segmentControl = (UISegmentedControl)target;
MyViewModel myViewModel = (MyViewModel)value;
segmentControl.SetTitle(myViewModel.MyFirstValue, 0);
segmentControl.SetTitle(myViewModel.MySecondValue, 1);
}
}
And use it in your view like so.
set.Bind (MySegmentControl).For ("Title").To ((MyViewModel vm) => vm);
Related
I'm trying to create a viewmodel provider factory and I'm little bit lost. I've already added the required Nuget packages and my view models extend the AndroidViewModel type. Now, I'd like to create a factory that would use autofac to create the required view models from the OnCreate activitie's method. The creation call looks like this:
_viewModel = (ViewModelProviders.Of(this, _viewModelFactory)
.Get(Java.Lang.Class.FromType(typeof(MainActivityViewModel))) as JavaObjectWrapper<MainActivityViewModel>)
.Object;
Now, the factory:
public class ViewModelFactory : ViewModelProvider.AndroidViewModelFactory {
public ViewModelFactory(Application application) : base(application) {
}
public override Object Create(Class modelClass) {
// TODO: any way to get the .NET type that was passed here?
return base.Create(modelClass);
}
}
Can I retrieve the .NET type (MainActivityViewModel) from the Class instance that is passed into the Create method call (the type would be required to resolve it from the autofac container)? If there is, how can I do that?
Thanks.
This is how I do this with Unity, but this pattern can be used for passing anything through the ViewModel constructor:
The ViewModel itself
public class HomeViewModel : ViewModel
{
IUnityContainer _unityContainer;
public HomeViewModel(IUnityContainer unityContainer)
{
_unityContainer = unityContainer;
}
}
The HomeViewModelFactory (Default constructor required)
public class HomeViewModelFactory : Java.Lang.Object, ViewModelProvider.IFactory
{
IUnityContainer _unityContainer;
public HomeViewModelFactory()
{
}
public HomeViewModelFactory(IUnityContainer unityContainer)
{
_unityContainer = unityContainer;
}
public Java.Lang.Object Create(Class p0)
{
return _unityContainer.Resolve<HomeViewModel>();
}
}
Usage in Fragment
public override void OnActivityCreated(Bundle savedInstanceState)
{
base.OnActivityCreated(savedInstanceState);
var homeViewModelFactory = _unityContainer.Resolve<HomeViewModelFactory>();
_homeViewModel = ViewModelProviders.Of(this, homeViewModelFactory).Get(Java.Lang.Class.FromType(typeof(HomeViewModel))) as HomeViewModel;
}
I have a model from my json in a Xamarin MVVM app(ios). I want to add the "%" after the value? in the list "coinmarketcaplist" contains the value 24h_change, this is the value I want to add a % to, it's a string. I know that I should use a getter for it, but I don't know how since I'm fairly new to this. below is my ViewModel code:
public class CMCTableViewModel : MvxViewModel
{
protected readonly ICoinMarketCapService _coinMarketCapService;
public CMCTableViewModel(ICoinMarketCapService coinMarketCapService)
{
_coinMarketCapService = coinMarketCapService;
LoadData();
}
private List<CoinMarketCapModel> _coinMarketCapModelList;
public List<CoinMarketCapModel> CoinMarketCapModelList
{
get
{
return _coinMarketCapModelList;
}
set
{
_coinMarketCapModelList = value;
RaisePropertyChanged(() => CoinMarketCapModelList);
}
}
public async void LoadData()
{
CoinMarketCapModelList = await _coinMarketCapService.GetCoins();
}
}
TableCell:
internal static readonly NSString Identifier = new NSString("CMCTableCell");
public override void LayoutSubviews()
{
base.LayoutSubviews();
MvxFluentBindingDescriptionSet<CMCTableCell, CoinMarketCapModel> set = new MvxFluentBindingDescriptionSet<CMCTableCell, CoinMarketCapModel>(this);
set.Bind(lblName).To(res => res.Name);
set.Bind(lblPrice).To(res => res.percent_change_24h);
set.Bind(imgCoin)
.For(img => img.Image)
.To(res => res.image)
.WithConversion<StringToImageConverter>();
set.Apply();
}
}
edit: added cellview
Use a converter in your binding:
1) Define converter:
public class StringFormatValueConverter : MvxValueConverter
{
public override object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value == null)
return null;
if (parameter == null)
return value;
return string.Format(parameter.ToString(), value);
}
}
2) Use it in your binding:
set.Bind(lblPrice).To(res => res.percent_change_24h).WithConversion<StringFormatValueConverter>("{0} %");
You can use this converter when you want to modify the input string by adding something around it, for example unit or currency
I've been banging my head against this for two days now working with mvvmcross, and having never worked with iOS before I think there's just something I don't understand.
I created my main menu with a UICollectionView in a two-column 3-row grid, each item representing a different location the user can go to on tap. I can override "ItemSelected" from the UICollectionViewSource, but I can't access the actual ViewModel without passing a reference of it into the source on creation....which doesnt feel like the right way to do it to me.
[MvxRootPresentation(WrapInNavigationController = true)]
public partial class MainPageView : MvxViewController
{
private MenuCollectionSource _menuCollectionSource;
List<MainMenuItem> menuItems;
public MainPageViewModel VM
{
get { return DataContext as MainPageViewModel; }
}
private void SetupMenuCollectionView()
{
......
collectionView.RegisterNibForCell(MainMenuCollectionViewCell.Nib, MainMenuCollectionViewCell.Key);
MainMenuItem.Init(menuItems);
_menuCollectionSource = new MenuCollectionSource(collectionView, MainMenuCollectionViewCell.Key, menuItems);
_menuCollectionSource.VM = VM; <----doesnt seem right.
collectionView.Source = _menuCollectionSource;
public class MenuCollectionSource : MvxCollectionViewSource
{
private UICollectionView _collectionView;
public List<MainMenuItem> Items { get; set; }
private MainPageViewModel _vm;
public MainPageViewModel VM
{
get { return _vm; }
set { _vm = value; }
}
}
With this method I can override ItemSelected in the ViewSource, and the do something like
( Cell is touched ->
Depending on cell enum/cell# - >
vm.NavigateToCorrectPage())
While this method works, I don't think its the correct way to handle this situation.
So then my next thought was to bind the source like like...(may not be 100%, trying to remember in my head)
set.CreateBinding(_menuCollectionSource) .For(s => s.SelectedCommand) .To(vm => vm.NavigateTo) .CommandParameter(_menuCollectionSource.SelectedItem)
But no matter what I tried, the passed param was always null as if the selected item was never set or the command was being called before it was set.
My CollectionViewCell class is pretty basic
public enum NavigationLocation
{
Search Database,
Lists,
etc....
}
public partial class MainMenuCollectionViewCell : MvxCollectionViewCell
{
public static readonly NSString Key = new NSString("MainMenuCollectionViewCell");
public static readonly UINib Nib;
public string MainMenuLabel
{ get { return mainMenuLabel.Text; } }
public int MainMenuIndexNumber
{ get; set; }
protected MainMenuCollectionViewCell(IntPtr handle) : base(handle)
{
}
static MainMenuCollectionViewCell()
{
Nib = UINib.FromName("MainMenuCollectionViewCell", NSBundle.MainBundle);
}
public static MainMenuCollectionViewCell Create()
{
NSArray topLevelObjects = NSBundle.MainBundle.LoadNib("MainMenuCollectionViewCell", null, null);
MainMenuCollectionViewCell cell = Runtime.GetNSObject(topLevelObjects.ValueAt(0)) as MainMenuCollectionViewCell;
return cell;
}
internal void BindData(string label, string iconBundleName)
{
mainMenuLabel.Text = label;
mainMenuImage.Image = UIImage.FromBundle(iconBundleName);
}
}
No binding I've tried in the cell class has actually worked, even adding a UITapGestureRecognizer on creation caused a crash on actual tap. I've run out of ideas, does anyone know what I'm not understanding or missing to actually implement
( Cell is touched ->
GetCellMenuType - >
CallCorrectCommandFromViewModel)
Thank you
Use EventHandler SelectedItemChanged from MvxBaseCollectionViewSource
View
set.Bind(yourCollectionViewSource).For(s => s.SelectionChangedCommand).To(vm => vm.CollectionItemSelected);
ViewModel
public ICommand CollectionItemSelected => new MvxCommand<ItemViewModel>((selectedItem) => { });
I'm trying out MvvmCross with Xamarin 'classic'.
I've got it working with Android.
But I can't get it work for iOS. I've taken a look at the sample mentioned here (eh): MVVMCross support for Xamarin.iOS Storyboards
I'm really missing something.
What do i have:
A storyboard with only 3 controls on it. a label and 2 buttons. All 3
have names so i get the properties in the RootViewController class.
The basis setup.cs
AppDelegate.cs
[Register("AppDelegate")]
public partial class AppDelegate : MvxApplicationDelegate
{
UIWindow _window;
public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{
_window = new UIWindow(UIScreen.MainScreen.Bounds);
StoryBoardTouchViewPresenter sbPresenter = new StoryBoardTouchViewPresenter(this, _window, "MainStoryboard");
var setup = new Setup(this, _window);
setup.Initialize();
var startup = Mvx.Resolve<IMvxAppStart>();
startup.Start();
sbPresenter.MasterNavigationController.NavigationBar.Translucent = false;
sbPresenter.MasterNavigationController.SetNavigationBarHidden(false, false);
return true;
}
}
StoryBoardTouchViewPresenter (from MVVMCross: Is it possible to use Storyboard with ICommand navigation?) But the API is changed.
public class StoryBoardTouchViewPresenter : MvxTouchViewPresenter
{
public static UIStoryboard Storyboard = null;
public StoryBoardTouchViewPresenter(UIApplicationDelegate applicationDelegate, UIWindow window, string storyboardName, NSBundle StoryboardBundleOrNull = null)
: base(applicationDelegate, window)
{
Storyboard = UIStoryboard.FromName(storyboardName, StoryboardBundleOrNull);
}
public override void Show(IMvxTouchView view)
{
MvxViewController sbView = null;
try
{
sbView = (MvxViewController)Storyboard.InstantiateViewController(view.Request.ViewModelType.Name.Replace("Model", ""));
}
catch (Exception e)
{
Console.WriteLine("Failed to find storyboard view, did you forget to set the Storyboard ID to the ViewModel class name without the Model suffix ?" + e);
}
sbView.Request = view.Request;
base.Show(sbView);
}
}
The default App.cs in the Core project
public class App : Cirrious.MvvmCross.ViewModels.MvxApplication
{
public override void Initialize()
{
CreatableTypes()
.EndingWith("Service")
.AsInterfaces()
.RegisterAsLazySingleton();
RegisterAppStart<ViewModels.MainViewModel>();
}
}
The ViewModel:
public class MainViewModel : MvxViewModel
{
ITodoTaskService taskService;
IDataManager<TodoTask> tasks;
public MainViewModel(ITodoTaskService taskService)
{
this.taskService = taskService;
}
public async override void Start()
{
this.tasks = new DataManager<TodoTask>(await this.taskService.GetTodoTasksAsync());
this.tasks.MoveFirst();
Rebind();
base.Start();
}
private void Rebind()
{
this.Description = this.tasks.Current.Description;
NextCommand.RaiseCanExecuteChanged();
PreviousCommand.RaiseCanExecuteChanged();
}
private string description;
public string Description
{
get { return this.description; }
set
{
this.description = value;
RaisePropertyChanged(() => Description);
}
}
private MvxCommand nextCommand;
public MvxCommand NextCommand
{
get
{
this.nextCommand = this.nextCommand ?? new MvxCommand(NavigateToNext, CanNavigateNext);
return this.nextCommand;
}
}
private bool CanNavigateNext()
{
return this.tasks.CanMoveNext;
}
public void NavigateToNext()
{
this.tasks.MoveNext();
Rebind();
}
private MvxCommand previousCommand;
public MvxCommand PreviousCommand
{
get
{
this.previousCommand = this.previousCommand ?? new MvxCommand(NavigateToPrevious, CanNavigatePrevious);
return this.previousCommand;
}
}
private bool CanNavigatePrevious()
{
return this.tasks.CanMovePrevious;
}
public void NavigateToPrevious()
{
this.tasks.MovePrevious();
Rebind();
}
}
I tried all kind of things. At the moment i get an exception that the MainView cannot be found. Which i partly understand. in App.cs MainViewModel is the start up. But the controller is called RootViewController. I think the RootviewController should bind to my MainViewModel. But i don't know how.
How should I make MvvmCross with iOs working?
How should I name the parts?
MvvmCross' default view finder will look for a view called MainView. That view should be derived from MvxViewController or another IMvxTouchView type. If you don't want to name your view controller "MainView" then you need to create a custom view resolver.
My advice: just rename your RootViewController to MainView.
I'm trying to generate an Html.ActionLink with the following viewmodel:
public class SearchModel
{
public string KeyWords {get;set;}
public IList<string> Categories {get;set;}
}
To generate my link I use the following call:
#Html.ActionLink("Index", "Search", Model)
Where Model is an instance of the SearchModel
The link generated is something like this:
http://www.test.com/search/index?keywords=bla&categories=System.Collections.Generic.List
Because it obviously is only calling the ToString method on every property.
What I would like to see generate is this:
http://www.test.com/search/index?keywords=bla&categories=Cat1&categories=Cat2
Is there any way I can achieve this by using Html.ActionLink
In MVC 3 you're just out of luck because the route values are stored in a RouteValueDictionary that as the name implies uses a Dictionary internally which makes it not possible to have multiple values associated to a single key. The route values should probably be stored in a NameValueCollection to support the same behavior as the query string.
However, if you can impose some constraints on the categories names and you're able to support a query string in the format:
http://www.test.com/search/index?keywords=bla&categories=Cat1|Cat2
then you could theoretically plug it into Html.ActionLink since MVC uses TypeDescriptor which in turn is extensible at runtime. The following code is presented to demonstrate it's possible, but I would not recommend it to be used, at least without further refactoring.
Having said that, you would need to start by associating a custom type description provider:
[TypeDescriptionProvider(typeof(SearchModelTypeDescriptionProvider))]
public class SearchModel
{
public string KeyWords { get; set; }
public IList<string> Categories { get; set; }
}
The implementation for the provider and the custom descriptor that overrides the property descriptor for the Categories property:
class SearchModelTypeDescriptionProvider : TypeDescriptionProvider
{
public override ICustomTypeDescriptor GetTypeDescriptor(
Type objectType, object instance)
{
var searchModel = instance as SearchModel;
if (searchModel != null)
{
var properties = new List<PropertyDescriptor>();
properties.Add(TypeDescriptor.CreateProperty(
objectType, "KeyWords", typeof(string)));
properties.Add(new ListPropertyDescriptor("Categories"));
return new SearchModelTypeDescriptor(properties.ToArray());
}
return base.GetTypeDescriptor(objectType, instance);
}
}
class SearchModelTypeDescriptor : CustomTypeDescriptor
{
public SearchModelTypeDescriptor(PropertyDescriptor[] properties)
{
this.Properties = properties;
}
public PropertyDescriptor[] Properties { get; set; }
public override PropertyDescriptorCollection GetProperties()
{
return new PropertyDescriptorCollection(this.Properties);
}
}
Then we would need the custom property descriptor to be able to return a custom value in GetValue which is called internally by MVC:
class ListPropertyDescriptor : PropertyDescriptor
{
public ListPropertyDescriptor(string name)
: base(name, new Attribute[] { }) { }
public override bool CanResetValue(object component)
{
return false;
}
public override Type ComponentType
{
get { throw new NotImplementedException(); }
}
public override object GetValue(object component)
{
var property = component.GetType().GetProperty(this.Name);
var list = (IList<string>)property.GetValue(component, null);
return string.Join("|", list);
}
public override bool IsReadOnly { get { return false; } }
public override Type PropertyType
{
get { throw new NotImplementedException(); }
}
public override void ResetValue(object component) { }
public override void SetValue(object component, object value) { }
public override bool ShouldSerializeValue(object component)
{
throw new NotImplementedException();
}
}
And finally to prove that it works a sample application that mimics the MVC route values creation:
static void Main(string[] args)
{
var model = new SearchModel { KeyWords = "overengineering" };
model.Categories = new List<string> { "1", "2", "3" };
var properties = TypeDescriptor.GetProperties(model);
var dictionary = new Dictionary<string, object>();
foreach (PropertyDescriptor p in properties)
{
dictionary.Add(p.Name, p.GetValue(model));
}
// Prints: KeyWords, Categories
Console.WriteLine(string.Join(", ", dictionary.Keys));
// Prints: overengineering, 1|2|3
Console.WriteLine(string.Join(", ", dictionary.Values));
}
Damn, this is probably the longest answer I ever give here at SO.
with linq of course...
string.Join("", Model.Categories.Select(c=>"&categories="+c))