I am using Xamarin to develop ios and android apps on one platefrom, c# is the language used.
I have a design that mimics the one below. A manager class that handles operations done to a collection, a Model class that groups data, and a View that displays the models it gets from the manager. I am displaying the models in a table, the view below is how I am doing so on iOS.
The view subscribes to any changes done to the collection in the manager and triggers a view update whenever one occurs. The model is responsible for removing itself from the collection if it is no longer used after a certain amount of time, thus I pass a reference to the list when the model is created.
This pattern is working on android devices but doesn't seem to work when ran on iOS. I've traced the problem to the removeDevice in the model class but can't seem to figure out why it isn't working. My guess if a pass by reference / value problem but I am not sure. Is there something obvious I am missing or do I need to use a different pattern for iOS?
New Information
After experimenting more this seems relevant. The iOS table is in a tab view and not the initial tab which means ViewController is not initialized until the tab is clicked. I found that if I didn't click the tab it would remove the Models from the list. Something else interesting is if I removed the CollectionChanged subscription it would as well.
public partial class ViewController: UIViewController
{
private List<Model> itemList;
private BlueTooth ble;
private UITableView deviceTable;
public ViewController (IntPtr handle) : base (handle)
{
ble = BlueTooth.Instance;
}
public override void ViewDidLoad ()
{
manager = ble.getManager();
itemList = manager.modelList.ToList();
manager.modelList.CollectionChanged += UpdateView;
deviceTable = new UITableView
{
Source = new DeviceTableSource(itemList)
};
View.AddSubview(deviceTable);
}
void UpdateView(object sender, EventArgs e)
{
itemList = ble.getManager().ModelList.ToList();
deviceTable.Source = new DeviceTableSource(itemList);
deviceTable.ReloadData();
}
}
Manager Class
class Manager{
public ObservableCollection<Model> modelList {get; set;}
public Manager(){
modelList= new ObervableCollection<Model>();
}
public addModel(){
Create a Model object with reference to ModelList
Add Model to ModelList
}
public updateModel(){
update Model in List
}
}
Model Class
class Model{
private ObservableCollection<Model> list;
private Timer removeTimer;
Model(modelList){
removeTimer += onTimedEvent
}
OnTimedEvent(){
removeDevice();
}
removeDevice(){
modelList.remove(this);
}
}
Related
I setup a very simple app to get me started and trying things out. It has a label and two buttons. The buttons are linked to relaycommands in the main view model are used to send a message to a server using Mqtt which work as intended. The label is used to show part of the data received from the server. Everything seems to work fine except the labels won't update as soon as the message is received event though I can see in debugging that the property is set. The label will update as soon as I press one of the two buttons...
I'm new to the whole Xamarin android thing and have used mvvm light once in a WPF application.
Main Activity :
public partial class MainActivity
{
// UI Elements
public TextView ScanInfoLabel { get; private set; }
public Button UnlockButton { get; private set; }
public Button RegisterButton { get; private set; }
// Keep track of bindings to avoid premature garbage collection
private readonly List<Binding> _bindings = new List<Binding>();
// Get view model
private MainViewModel mainViewModel { get { return App.Locator.Main;}}
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
// Set view from the "main" layout resource
SetContentView(Resource.Layout.Main);
// Get the UI elements by ID
ScanInfoLabel = FindViewById<TextView>(Resource.Id.ScanInfoLabel);
UnlockButton = FindViewById<Button>(Resource.Id.UnlockButton);
RegisterButton = FindViewById<Button>(Resource.Id.RegisterButton);
// Set Bindings for textviews
_bindings.Add(
this.SetBinding(
() => mainViewModel.ScanInfoLabel,
() => ScanInfoLabel.Text));
// Set the bindings for commands
UnlockButton.SetCommand("Click", mainViewModel.UnlockCommand);
RegisterButton.SetCommand("Click", mainViewModel.RegisterTagCommand);
}
In Main view model :
// RelayCommands
public RelayCommand UnlockCommand;
public RelayCommand RegisterTagCommand;
public RelayCommand MqttConnectCommand;
// Bindable properties
private string _scanInfoLabel = "Test";
public string ScanInfoLabel
{
get { return _scanInfoLabel; }
set { Set(ref _scanInfoLabel, value); }
}
// New scan message received
private void RFIDScanReceived(RFID.Scan scan)
{
ScanInfoLabel = BitConverter.ToString(scan.UID);
}
I would expect the label to show the data as soon as the mqtt message is received (which is then sent to the mainviewmodel using Messenger.Default.send<>() from mvvm light). But nothing is changed in the UI until I click on one of the buttons and then the correct information in displayed.
I don't really know where to start being new to xamarin android and none of my searches seemed to be of any help.
Any help will be appreciated, thanks!
ViewModels generally implement the INotifyPropertyChanged interface,
which means that the class fires a PropertyChanged event whenever one
of its properties changes. The data binding mechanism in Xamarin.Forms
attaches a handler to this PropertyChanged event so it can be notified
when a property changes and keep the target updated with the new
value.
Solution:
Make you model inherit from INotifyPropertyChanged and add PropertyChanged inside the set part. Then the labels will update as soon as the value of ScanInfoLabel changed.
public class BaseViewModel : INotifyPropertyChanged
{
// Bindable properties
private string _scanInfoLabel = "Test";
public string ScanInfoLabel
{
get { return _scanInfoLabel; }
set
{
_scanInfoLabel = ScanInfoLabel;
PropertyChanged(this, new PropertyChangedEventArgs("ScanInfoLabel"));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
You can refer:data-bindings-to-mvvm
Well I found out it was a threading issue since I set the property from a Messenger call. This apparently is not an issue in WPF which is why I was a bit stuck but using the DispatcherHelper did the trick.
// New scan message received
private void RFIDScanReceived(RFID.Scan scan)
{
DispatcherHelper.CheckBeginInvokeOnUI(() =>
{
ScanInfoLabel = BitConverter.ToString(scan.UID);
});
}
I'm currently trying to create a "log" text box that gets messages between multiple view models (tied to multiple views) that I have. I've tried the approach described by user Blachshma here (Multiple Data Contexts in View) but it does not seem to be working.
I have three classes. Class AViewModel, Class BViewModel and Class ABViewModel.
The view for A binds to AViewModel using the following code in its constructor:
this.InitializeComponent();
this.model = new AViewModel();
this.DataContext = this.model;
The view for B and AB follows the same pattern.
The class structures are as follows:
public class A : INotifyPropertyChanged
{
private string log = string.empty;
public class A()
{
}
public string ALog
{
get
{
return this.log;
}
set
{
this.log = value;
this.NotifyPropertyChanged("ALog");
}
}
private void NotifyPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
/* Function that executes when relay command is clicked */
private void ExecuteCommand()
{
this.ALog += "here";
}
}
Class B is defined the same way with property BLog
Class ABViewModel has properties for each other view model
public class ABViewModel
{
public AViewModel AVM
{
get;
set;
}
public BViewModel BVM
{
get;
set;
}
}
In the xaml I simply have
<TextBox Text="{Binding ABViewModel.AVM}" />
My plan is to eventually using Multibinding to concatenate both logs together, but at the moment I can't even get the one View Model to update my string. It looks like my container view model ABViewModel isn't getting updated, but I don't really understand why, but I'm not entirely sure how to fix this.
Any suggestions are extremely appreciated!
Thanks!
Edit:
I debug my code and see that my string ALog is getting updated, but I don't see the change on the UI. For more information, I click a button that's connected to a RelayCommand in class A. This button invokes a method to connect to a COM port. I'm able to use the COM port from other view models successfully after opening it. The log is supposed to update saying that the com port was opened but I never see any text added to the log in the GUI even though the instance of ALog that I can debug through has the added text.
I can't use Prism or MVVM-light for this particular project.
I have done some searching and I can't find anyone with my specific problem.
I have a Caliburn.Micro project and I successfully have a main view with sub-views inside it which is not a problem. My View Models are in a different assembly to my views.
This meant I had to override SelectAssemblies to include my view models project:
protected override IEnumerable<Assembly> SelectAssemblies()
{
var assemblies = base.SelectAssemblies().ToList();
assemblies.Add(typeof(OrderViewModel).Assembly);
return assemblies;
}
Now, this is where my confusion starts. I successfully have a OrderView showing the OrderViewModel. Inside that there is a KeyboardViewModel with a KeyboardView. This all works fine so caliburn is finding the right assemblies etc.
However when I come to use the window manager to display a new view/viewmodel which is passed into the order view. I am getting a screen with the text "Cannot find view model for XX.ViewModels.Model."
This is my OrderViewModel
[Export(typeof(OrderViewModel))]
public class OrderViewModel : Screen
{
private readonly IWindowManager windowManager;
private ISession session;
[ImportingConstructor]
public OrderViewModel(IWindowManager windowManager, KeyboardViewModel keyboardViewModel)
{
TillDatabase.CreateInstance(ApplicationConfiguration.Instance.DatabaseConnectionString);
this.windowManager = windowManager;
this.Keyboard = keyboardViewModel;
this.Keyboard.Order = this;
this.Keyboard.Home();
}
public void ChangePriceBand()
{
windowManager.ShowWindow(new PriceBandSelectionViewModel(this));
}
}
The thing is, I even tried this in ChangePriceBand
windowManager.ShowWindow(new OrderViewModel(this.windowManager, new KeyboardViewModel()));
And this gets the same error. Even though a view has already been associated with the OrderViewModel previously!!
This is the PriceBandSelectionViewModel just in case.
[Export(typeof(PriceBandSelectionViewModel))]
public class PriceBandSelectionViewModel : Screen
{
private OrderViewModel order;
[ImportingConstructor]
public PriceBandSelectionViewModel(OrderViewModel order)
{
this.order = order;
}
public ObservableCollection<PriceBandButtonViewModel> Buttons
{
get
{
var list = new ObservableCollection<PriceBandButtonViewModel>();
var priceBands = this.order.Session.QueryOver<Application_Model_PriceBand>().List();
foreach (var priceBand in priceBands)
{
PriceBandButtonViewModel button = new PriceBandButtonViewModel(priceBand, this);
list.Add(button);
}
return list;
}
}
public void ProcessButtonClick(Application_Model_PriceBand button)
{
this.order.ChangeCurrentPriceBand(button);
base.TryClose();
}
}
I'm just really confused to how Caliburn is setting up my main view, but the window manager isn't even though its the same ViewModel?
have you tried to remove OrderViewModel or put a breakpoint there, cant find view error might happen if it encountered error when initialising the exported class
public PriceBandSelectionViewModel()
{
// this.order = order;
}
or add
assemblies.Add(typeof(PriceBandSelectionViewModel).Assembly);
This may be the same problem as I am experiencing as described here: Caliburn.Micro HelloWindowManager Sample - View location not working
To see if it is the same problem, try changing the call from
windowManager.ShowWindow(new PriceBandSelectionViewModel(this));
to
windowManager.ShowDialog(new PriceBandSelectionViewModel(this));.
In my case, ShowDialog was able to locate the view no problem, but ShowWindow and ShowPopup were not.
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.
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)