For the past couple of weeks I've been working on developing a cross platform app (IOS/Android/WP7) using the MVVMCross framework. Today I ran into a problem I don't really know how to solve, so hopefully you can push me in the right direction.
In the IOS I have the following construction for navigating to another page (the code below is located in a ViewModel):
KeyValuePair<string,string> kvpAct1 = new KeyValuePair<string, string>("short", ".countertest5");
public IMvxCommand BeckhoffActuator1
{
get
{
return new MvxRelayCommand<Type>((type) => this.RequestNavigate<Beckhoff.BeckhoffActuatorViewModel>(kvpAct1));
}
}
When this IMvxCommand is fired (button pressed) the next View is loaded, in this case the BeckhoffActuatorViewModel. In the code of the BeckhoffActuatorView I use the keyvaluepair from above:
public class BeckhoffActuatorView : MvxTouchDialogViewController<BeckhoffActuatorViewModel>
{
ICollection<string> icol;
public BeckhoffActuatorView(MvxShowViewModelRequest request) : base(request, UITableViewStyle.Grouped, null, true)
{
icol = request.ParameterValues.Values;
}
public override void ViewDidLoad()
{
//Code
}
}
This construction is working fine in IOS, but I would like to use the same construction in my android App.
The code in the ViewModel hasn't changed since that's the whole idea of MVVM. But the code of the BackhoffActuatorView is different for Android:
public class BeckhoffActuatorView : MvxBindingActivityView<BeckhoffSensorViewModel>
{
public ICollection<string> icol;
public BeckhoffActuatorView()
{
Debug.WriteLine("Standard");
}
public BeckhoffActuatorView(MvxShowViewModelRequest request)
{
Debug.WriteLine("Custom");
icol = request.ParameterValues.Values;
}
protected override void OnViewModelSet()
{
SetContentView(Resource.Layout.BeckhoffActuatorView);
}
}
The code above isn't working, the MvxBindingActivityView doesn't seem to implement something similar to the ViewController I use in IOS. The code only come in the standard constructor, and when I leave that one out completely it won't compile/run.
Does anyone know know I can access the keyvaluepair I send with the RequestNavigate? Thank you!
MVVMCross is very convention based - and it works on the idea of passing messages between ViewModels wherever possible.
If you navigate to a ViewModel using:
KeyValuePair<string,string> kvpAct1 = new KeyValuePair<string, string>("short", ".countertest5");
public IMvxCommand BeckhoffActuator1
{
get
{
return new MvxRelayCommand<Type>((type) => this.RequestNavigate<Beckhoff.BeckhoffActuatorViewModel>(kvpAct1));
}
}
then you should be able to pick that up in the BeckhoffActuatorViewModel using the constructor:
public class BeckhoffActuatorViewModel : MvxViewModel
{
public BeckhoffActuatorViewModel(string short)
{
ShortValue = short;
}
private string _shortValue;
public string ShortValue
{
get
{
return _shortValue;
}
set
{
_shortValue = value;
FirePropertyChanged("ShortValue");
}
}
}
And your views can then access ViewModel.ShortValue (for iOS this can be done after base.ViewDidLoad(), for Android after OnCreate() and for WP7 after OnNavigatedTo)
For an example of this, take a look at the TwitterSearch example:
https://github.com/slodge/MvvmCrossTwitterSearch
This has a HomeViewModel which calls navigate using:
private void DoSearch()
{
RequestNavigate<TwitterViewModel>(new { searchTerm = SearchText });
}
and a TwitterViewModel which receives the searchTerm using the constructor:
public TwitterViewModel(string searchTerm)
{
StartSearch(searchTerm);
}
Please note that only strings are allowed in this message passing at present - but you can always serialise your own objects using JSON.Net - or you can extend the framework - it's open source.
Please note that only strings, ints, doubles and bools are allowed in this constructor parameter passing at present - this is due to serialisation requirements for Xaml Urls and for Android Intents. If you want to experiment with navigation using your own custom serialised objects, then please see http://slodge.blogspot.co.uk/2013/01/navigating-between-viewmodels-by-more.html.
Also, note that if you want to use the anonymous object navigation (RequestNavigate<TwitterViewModel>(new { searchTerm = SearchText });) then you will need to make sure that an InternalsVisibleTo attribute is set - see https://github.com/slodge/MvvmCrossTwitterSearch/blob/master/TwitterSearch.Core/Properties/AssemblyInfo.cs:
[assembly: InternalsVisibleTo("Cirrious.MvvmCross")]
Further... not for the faint-hearted... and this isn't "good mvvm code"... but if you really want/need to access the MvxShowViewModelRequest data inside an Android activity, then you can extract it from the incoming Intent - there's an Extras string containing the request (see the deserialisation in CreateViewModelFromIntent in https://github.com/slodge/MvvmCross/blob/master/Cirrious/Cirrious.MvvmCross/Android/Views/MvxAndroidViewsContainer.cs)
Related
I'm very newbie using MvvmCross and I have an issue. I want to implement taking a photo in an Activity, the native way, but I don't know how I can pass data to the ViewModel.
There is a way to do that?
Passing data to a ViewModel is fairly easy. If you are in the Activity and given that it inherits from MvxActivity or the likes you will have the ViewModel property you can access directly from it.
There are also generic versions of these activities, so you don't have to type cast the ViewModel property yourself.
So if you have:
public class MyActivity : MvxActivity<PictureViewModel>
{
}
Then you can access ViewModel:
ViewModel.SomeViewModelProperty = myData;
This myData object could be a byte array with the picture data.
MvvmCross does have a Picture taking plugin already, which uses the built in Android camera to take pictures with. If you add
MvvmCross.Plugins.PictureChooser
To both your Android and Core project, then you can use the IMvxPictureChooserTask directly in the ViewModel without involving anything else:
public class PictureViewModel : MvxViewModel
{
private IMvxPictureChooserTask _pictureTask;
public PictureViewModel(IMvxPictureChooserTask pictureTask)
{
_pictureTask = pictureTask;
}
private byte[] _pictureBytes;
// in some command:
private void DoTakePicture()
{
_pictureTask.TakePicture(500, 500, stream => {
_pictureBytes = ReadStream(stream);
}, () => {});
}
}
Does anyone know how to view an existing IMvxViewModel?
In my app, I have already created a bunch of ViewModels (PhotoViewModel) inside of another view model. They exist as a property on the parent ViewModel (AlbumViewModel). It would be very nice to just show a particular instance of a PhotoViewModel instead of creating a new instance of that view model when I want to view it.
public class AlbumViewModel : MvxViewModel {
public ObservableCollection<PhotoViewModel> Photos
{
get { return GetValue(() => Photos); }
set { SetValue(value, () => Photos); }
}
}
public class PhotoViewModel : MvxViewModel { }
I was wondering if there was a way, other then creating my own IMvxViewModelLocator, to accomplish this task. I think having a protected method on the MvxNavigationObject called View could be really helpful both for new developers using the framework as well as performance. We'd be able to skip all of the reflection done currently to instantiate a view model.
The default ShowViewModel mechanism in MvvmCross uses page-based navigation - this navigation has to use Uris on WindowsPhone and Intents on Android.
Because of this, MvvmCross does not allow navigation by 'rich' objects - simple serialisable POCOs are Ok, but complicated 'rich' objects are not supported.
This is further essential because of 'tombstoning' - if your app/page/activity is later rehydrated then you cannot be sure of what historic View or ViewModel objects are actually in your history "back" stack.
If you want to navigate by rich object then the best way is to store those rich objects in a lookup service and to then navigate by some key/index into the lookup. However, I would personally call those lookedup objects Models rather than ViewModels (but the boundary does sometimes become blurred!)
Although based on MvvmCross v1 code, this question still gives quite a good background to this - What is the best way to pass objects to "navigated to" viewmodel in MVVMCross?
Some more up-to-date explanations include:
How to pass data across screens using mvvmcross
Custom types in Navigation parameters in v3
https://github.com/slodge/MvvmCross/wiki/ViewModel--to-ViewModel-navigation (under construction)
One final thing....
... the MvvmCross manifesto insists that MvvmCross is very open to customisation ...
Because of this you can override MvvmCross navigation and view model location if you want to. To do this, creating your own IMvxViewModelLocator would probably be a good way to start.
After some testing, below is a proposed solution. I'm not 100% in love with it, but it does work and provide the type developer experience I was looking for. So lets dig in.
To start, all of my ViewModels (VM) inherit from a base VM, AVM. This abstract base class supports looking up of an object as a public static method. It's a little gross, but it works well if you're willing to sip on the Kool-Aid. Below is the portion of the class that's relevant to this problem:
public abstract class AVM : MvxViewModel {
private static readonly Dictionary<Guid, WeakReference> ViewModelCache = new Dictionary<Guid, WeakReference>();
private static readonly string BUNDLE_PARAM_ID = #"AVM_ID";
private Guid AVM_ID = Guid.NewGuid();
private Type MyType;
protected AVM()
{
MyType = this.GetType();
ViewModelCache.Add(AVM_ID, new WeakReference(this));
}
public static bool TryLoadFromBundle(IMvxBundle bundle, out IMvxViewModel viewModel)
{
if (null != bundle && bundle.Data.ContainsKey(BUNDLE_PARAM_ID))
{
var id = Guid.Parse(bundle.Data[BUNDLE_PARAM_ID]);
viewModel = TryLoadFromCache(id);
return true;
}
viewModel = null;
return false;
}
private static IMvxViewModel TryLoadFromCache(Guid Id)
{
if (ViewModelCache.ContainsKey(Id))
{
try
{
var reference = ViewModelCache[Id];
if (reference.IsAlive)
return (IMvxViewModel)reference.Target;
}
catch (Exception exp) { Mvx.Trace(exp.Message); }
}
return null;
}
protected void View()
{
var param = new Dictionary<string, string>();
param.Add(BUNDLE_PARAM_ID, AVM_ID.ToString());
ShowViewModel(MyType, param);
}
In order to get this all wired up, you have to create a custom view model locator. Here's the custom locator:
public class AVMLocator : MvxDefaultViewModelLocator
{
public override bool TryLoad(Type viewModelType, IMvxBundle parameterValues, IMvxBundle savedState, out IMvxViewModel viewModel)
{
if (AVM.TryLoadFromBundle(parameterValues, out viewModel))
return true;
return base.TryLoad(viewModelType, parameterValues, savedState, out viewModel);
}
}
Lastly you have to wire up. To do so, go into your App.cs and override CreateDefaultViewModelLocator like so:
protected override IMvxViewModelLocator CreateDefaultViewModelLocator()
{
return new AVMLocator();
}
You're all set. Now in any of your derived ViewModels that are already alive and well, you can do the following:
myDerivedVM.View();
There's still some more I need to do (like making sure the WeakReferences do their job and I don't have memory leaks and some additional error handling), but at the very least it's the experience I was going for. The last thing I did was add the following command to the AVM base class:
public MvxCommand ViewCommand
{
get { return new MvxCommand(View); }
}
Now you can bind that command to any UI object and when invoked, it'll launch that view with that very instance of the VM.
Stuart, thanks for your help in steering me in the right direction. I'd be interested in hearing your feedback on the solution I provided. Thanks for all of your work with MVVMCross. It really is a very beautiful bit of code.
Cheers.
I´ve got 2 ViewModels (ConfigurationViewModel and EditConfigurationViewModel). In the ConfigurationViewModel I've got the following code:
public ConfigurationViewModel()
{
NewConfigCommand = new MvxRelayCommand(DoNewConfig);
EditConfigCommand = new MvxRelayCommand<ConfigurationSet>(DoEditConfig);
}
private void DoNewConfig()
{
this.RequestNavigate<EditConfigurationViewModel>();
}
private void DoEditConfig(ConfigurationSet config)
{
this.RequestNavigate<EditConfigurationViewModel>(new { id = config.Id.ToString() });
}
In the EditConfigurationViewModel I've got the following code:
public EditConfigurationViewModel()
{
Configuration = new ConfigurationSet();
}
public EditConfigurationViewModel(string id)
{
Configuration = ConfigDataStore.GetConfiguration(Guid.Parse(id));
}
What I want to achieve is something very simple... In the ConfigurationViewModel when the NewConfigCommand is fired, I want to navigate to the EditConfigurationViewModel, and use the parameterless constructor. When the EditConfigCommand is fired I want to use the constructor that receives a string.
The problem with this code is that no matter what command is fired, the parameterless constructor is allways used and the code never reaches the other constructor.
I did some experiments, by removing the parameterless constructor, and the result was that the other constructor is called and I get the expected result for the EditConfigurationCommand, but if I try to fire the NewConfigurationCommand an exception is throw due too the inesxistence of a parameterless constructor (so far so good).
Unfortunately, at this moment I don't have VS2010 installed, so I'm not able to debug through PCL code... I've done some "eye debug" and found this class MvxViewModelLocator. I think the problem is somewhere here. Maybe in the DoLoad method when it tries to get the MethodInfo...
At this point I just wanted to know if I'm doing something wrong or if this is the expected result. Meanwhile I think I'll take a chance on installing VS2010 and pray that it won´t break anything...
On the PCL debugging issue, why not just add a Win8 or WP7/8 UI - then you can debug through the PCL code...
On the main question - about how to use multiple constructors... I'd suggest you don't.
For me, edit and new are two different views and two different viewmodels - they may share common properties and common layout - but this can be achieved using inheritance, using UserControls, using include axml, etc.
For an example of what I generally use for new and edit see https://github.com/slodge/MvvmCross/tree/vnext/Sample%20-%20CustomerManagement/CustomerManagement/CustomerManagement/ViewModels
If you do insist on carry on using one viewmodel, then you could consider using a 'magic value' for New - e.g. if Guid.Empty is passed then that means new?
Alternatively, you could just drop your parameterless constructor and could add a default value to the second one:
public EditConfigurationViewModel(string id = null)
{
Guid value;
if (id == null || !Guid.TryParse(id, out value))
{
Configuration = new ConfigurationSet();
}
else
{
Configuration = ConfigDataStore.GetConfiguration(value);
}
}
I think that would work?
Finally, if none of that seems suitable to you, then you could consider overriding the ViewModel construction mechanism.
To help with this, there's a fairly detailed recent post on how to write your own default ViewModelLocator for MvvmCross - see http://slodge.blogspot.co.uk/2013/01/navigating-between-viewmodels-by-more.html
Using this approach, you could create a much more custom navigation model - or if this is the only special view model, then I suspect you could create a default viewModelLocator like:
public class MyViewModelLocator
: MvxDefaultViewModelLocator
{
public override bool TryLoad(Type viewModelType, IDictionary<string, string> parameterValueLookup,
out IMvxViewModel model)
{
if (viewModelType == typeof(EditConfigurationViewModel))
{
string id;
if (parameterValueLookup.TryGetValue("id", out id))
{
model = new EditConfigurationViewModel(id);
}
else
{
model = new EditConfigurationViewModel();
}
return true;
}
return base.TryLoad(viewModelType, parameterValueLookup, IMvxViewModel model);
}
}
and register that locator in App.cs using:
protected override IMvxViewModelLocator CreateDefaultViewModelLocator()
{
return new MyViewModelLocator();
}
How do you implement a view with a grouped table in MonoTouch using MvvmCross, so you get something like this:
http://www.yetanotherchris.me/storage/downloads/UITableViewController.png
Right now I have this piece of code, but I cannot change the UITableViewStyle to Grouped:
public partial class HomeView : MvxBindingTouchTableViewController<HomeViewModel>
{
public HomeView(MvxShowViewModelRequest request)
: base(request)
{
}
public override void ViewDidLoad()
{
base.ViewDidLoad();
NavigationItem.SetRightBarButtonItem(new UIBarButtonItem("History", UIBarButtonItemStyle.Bordered, (sender, e) => ViewModel.DoGoToHistory()), false);
var source = new MvxActionBasedBindableTableViewSource(
TableView,
UITableViewCellStyle.Value1,
new NSString("HomeView"),
"{'TitleText':{'Path':'Date'},'DetailText':{'Path':'Location'},'SelectedCommand':{'Path':'ViewDetailCommand'}}",
UITableViewCellAccessory.DisclosureIndicator);
this.AddBindings(
new Dictionary<object, string>()
{
{ source, "{'ItemsSource':{'Path':'List'}}" },
{ this, "{'Title':{'Path':'TestTitle'}}"}
});
TableView.Source = source;
TableView.ReloadData();
}
}
Does anyone knows how to do this?
Your picture shows only one section.... assuming you are looking for just one section, but this grouped styling, then all you need to do is to introduce UITableViewStyle.Grouped somehow.
I'm not sure that the current MvxTableViewController exposes this for you - so you might either need to edit the Mvx source to add the appropriate constructors:
protected MvxTouchTableViewController(MvxShowViewModelRequest request, UITableViewStyle style = UITableViewStyle.Plain)
: base(style)
{
ShowRequest = request;
}
and
protected MvxBindingTouchTableViewController(MvxShowViewModelRequest request, UITableViewStyle style = UITableViewStyle.Plain)
: base(request, style)
{
}
Alternatively you could use a basic view controller (in which you add a Table as a subview) instead of a tableview derived view controller.
If you want multiple Groups, then you'll need to do a bit more work - as you'll need to work out how the bound TableViewSource works out the number of sections and the number of items in each section.
public class UserView : MvxTableViewController<UserViewModel>
{
public UserView()
:base(UITableViewStyle.Grouped)
{
}
}
Remember to make the constructor public and parameterless.
Working on a sideproject with WP8, but having trouble getting IsolatedStorage working. I have looked at dozens of posts seemingly asking the same question, but I haven't been able to get any of the solutions to work. The application is a simple task organizer where I have created my own Task Objects, one being a Summary Task and each SummaryTask containing a list of BasicTasks. I have tried using XMLSerializing only to run into problems because I was using an ObservableCollection. Thought I could change the collection to a Subclass of INotifyPropertyChanged but that didn't work either. Quite frankly, I'm still getting the hang of the different between the two anyways. So anyways, my latest attempt involves trying to use IsolatedStorage Settings and that didn't work either. Here is my class definition:
class SummaryTask : TaskItem
{
public List<BasicTask> children = new List<BasicTask>();
private string sumTaskName;
private int sumTaskId;
public SummaryTask()
{
}
public SummaryTask(string name, int id)
{
sumTaskName = name;
sumTaskId = id;
}
public string SumTaskName
{
get { return sumTaskName; }
set { sumTaskName = value; }
}
public int SumTaskId
{
get { return sumTaskId; }
set { sumTaskId = value; }
}
public void addTask(string taskName, string taskText, int taskId){
children.Add(new BasicTask(taskName, taskText, taskId));
}
public List<BasicTask> CHILDREN
{
get { return children; }
}
}
}
I create a list of this SummaryTask in a Global variable and use it throughout my pages for easy access. Here is what the beginning of my MainPage.xaml.cs file looks UPDATED:
public MainPage()
{
InitializeComponent();
BackKeyPress += OnBackKeyPressed;
if (Global.settings.Contains("list"))
{
Global.list = (List<SummaryTask>)Global.settings["list"];
}
else
{
Global.list = new List<SummaryTask>();
}
}
Guidance on the poor quality of my code and how to improve it is also accepted. Thank you.
Edit: The exception indicates that an item with the same key has already been created. The stacktrace doesn't show anything of importance in this case. I should also note that the exception is thrown after adding an object to the list and trying to save it, not while compiling.
The piece of code I am using to try to save to the Isolated Storage is here, it triggers when I navigate to MainPage.xaml:
protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e)
{
resultList.SelectedItem = null;
Global.settings["list"] = Global.list;
Global.settings.Save();
}
No exceptions anymore, but exiting the app and reentering isn't pulling up any saved data.
The problem with Add is very simple to fix - just use the indexer instead, which allows you to overwrite an entry with the same name:
settings["list"] = Global.list;
That won't fix the Save call... but you'd need to give more details about what exception (not just "it tells me", the full exception details) to help us help you more.