ItemsSource is empty in constructor of Custom Bound Horizontal ScrollView - c#

I'm having trouble updating Cheesebarons horizontal list view to the new v3 MvvmCross. Everything seems to be working correctly except in the constructor of my "BindableHorizontalListView" control the ItemsSource of the adapter is null. Which is weird because the context shows that the view-model property I am attempting to bind to shows very clearly there are 3 items and the binding seems very straightforward. What am I missing? I hope I've included enough of the code. I've also tried binding it via fluent bindings on the "OnViewModelSet" event with the same result.
Warning presented
[MvxBind] 24.87 Unable to bind: source property source not found Cirrious.MvvmCross.Binding.Parse.PropertyPath.PropertyTokens.MvxPropertyNamePropertyToken on DeviceViewModel
AXML
<BindableHorizontalListView
android:id="#+id/listView"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
local:MvxBind="ItemsSource DevicesList; ItemClick ItemSelected"
local:MvxItemTemplate="#layout/devices_horizontal_list_item" />
BindableHorizontalListView control
using System.Collections;
using System.Windows.Input;
using Android.Content;
using Android.Util;
using Cirrious.MvvmCross.Binding.Attributes;
using Cirrious.MvvmCross.Binding.Droid.Views;
namespace Project.Droid.Controls
{
public class BindableHorizontalListView
: HorizontalListView //Which inherits from AdapterView<BaseAdapter>
{
public BindableHorizontalListView(Context context, IAttributeSet attrs)
: this(context, attrs, new MvxAdapter(context))
{
}
public BindableHorizontalListView(Context context, IAttributeSet attrs, MvxAdapter adapter)
: base(context, attrs)
{
InitView ();
var itemTemplateId = MvxAttributeHelpers.ReadListItemTemplateId (context, attrs);
adapter.ItemTemplateId = itemTemplateId;
Adapter = adapter;
SetupItemClickListener();
}
public new MvxAdapter Adapter
{
get { return base.Adapter as MvxAdapter; }
set
{
var existing = Adapter;
if (existing == value)
return;
if (existing != null && value != null)
{
value.ItemsSource = existing.ItemsSource;
value.ItemTemplateId = existing.ItemTemplateId;
}
base.Adapter = value;
}
}
[MvxSetToNullAfterBinding]
public IEnumerable ItemsSource
{
get { return Adapter.ItemsSource; }
set { Adapter.ItemsSource = value; this.Reset (); }
}
public int ItemTemplateId
{
get { return Adapter.ItemTemplateId; }
set { Adapter.ItemTemplateId = value; }
}
public new ICommand ItemClick { get; set; }
private void SetupItemClickListener()
{
base.ItemClick += (sender, args) =>
{
if (null == ItemClick)
return;
var item = Adapter.GetItem(args.Position) as Java.Lang.Object;
if (item == null)
return;
if (item == null)
return;
if (!ItemClick.CanExecute(item))
return;
ItemClick.Execute(item);
};
}
}
}
View
[Activity (Label = "Device", ScreenOrientation = ScreenOrientation.Portrait)]
public class DeviceView : MvxActivity
{
protected override void OnCreate (Bundle bundle)
{
base.OnCreate (bundle);
SetContentView(Resource.Layout.device);
}
}
Property on ViewModel
private Services.Device[] _devicesList;
public Services.Device[] DevicesList {
get {
return _devicesList;
}
set {
_devicesList = value;
RaisePropertyChanged(() => DevicesList);
}
}
If only there was PCL support in XAM STUDIO I would just step in and see how the other controls are doing it!!!!

ItemsSource will always be empty in the constructor - it's a property which is set by binding, and that property can only be set after the constructor has completed.
The message:
[MvxBind] 24.87 Unable to bind: source property source not found Cirrious.MvvmCross.Binding.Parse.PropertyPath.PropertyTokens.MvxPropertyNamePropertyToken on DeviceViewModel
contains a bug - fixed in a recent commit - so the message should be more readable in the future.
If the bug hadn't been there, I suspect the message would say that the problem is in DevicesList - the binding can't find that property. Is it there? Does it have a get? Is it public?

Related

MVVM adding a list from a static service into the view?

I have created a service (static methods), which will get for instance all folders within google drive and returning a List<File>. (those methods are async MSDN Async Programming)
The problem is I dont know how to pass my results into the view. I tried to using a ObservableCollection but I cant make it work.
And one more thing is that Im not sure if its in my usage useful.
I dont add one item or delete one item. I just scrap the whole folders every refresh. What I have understood is that this is useful for a collection of data which will edited by the user.
public class MainWindowViewModel : ViewModelBase
{
public MainWindowViewModel()
{
var service = DriveHelper.createDriveService("client_secret.json", false)
// ERROR
_googleDriveFolders = new NotifyTaskCompletion<List<File>>( DriveHelper.getFiles(service), "trashed=false and mimeType = 'application/vnd.google-apps.folder'"));
}
public NotifyTaskCompletion<List<File>> googleDriveFolders { get; private set; }
private ObservableCollection<File> _googleDriveFolders;
public ObservableCollection<File> googleDriveFolder
{
get { return _googleDriveFolders; }
set
{
_googleDriveFolders = value;
RaisePropertyChanged();
}
}
//...
As noted in the comments, the issue is that your NotifyTaskCompletion immediately returns and gets assigned to the refreshFoldersCommand (btw. in C# naming conventions properties are in Pascal Case aka Camel Uppercase, not Camel lower case notation) property and the event is raised up immediately and not after the async operation finishes.
It's very bad practice to put async code into the ViewModels constructor (or any constructor for that case), because inside a constructor you can't await the async method.
There is no easy solution for it. The correct solution requires you to change your applications architecture and make use of a navigation service. I've posted it a few times already here on StackOverflow.
Prism (Microsoft's MVVM Framework) does come with a clean solution. It has an INavigationAware interface, that contains 3 methods (OnNagivatedTo, OnNavigatedFrom and IsNavigatioNTarget). To async load data into your ViewModel, NavigateTo is the important one.
In Prism it is called, after the previous View was unloaded (after calling NavigateFrom in the former ViewModels class) and the newly one has been instantiated and assigned to the new View. Parameters passed to theNavigationService.Navigate(..)method are passed toOnNagivatedTo` method of the ViewModel.
It can be marked as async and you can put your code there and await it
public class MainWindowViewModel : ViewModelBase
{
public MainWindowViewModel()
{
}
public NotifyTaskCompletion<List<File>> googleDriveFolders { get; private set; }
private ObservableCollection<File> _googleDriveFolders;
public ObservableCollection<File> googleDriveFolder
{
get { return _googleDriveFolders; }
set
{
_googleDriveFolders = value;
RaisePropertyChanged();
}
}
public async void OnNavigatedTo(NavigationContext context)
{
var service = DriveHelper.createDriveService("client_secret.json", false)
// ERROR
googleDriveFolder = await DriveHelper.getFiles(service), "trashed=false and mimeType = 'application/vnd.google-apps.folder'");
}
...
}
Edit:
Further answers about the same issue:
Pass parameter to a constructor in the ViewModel
How can I open another view in WPF MVVM using click handlers and commands? (Is my solution reasonable?)
Edit 2:
Also, you are assigning your NotifyTaskCompletion to _googleDriveFolders which is your backing field for the googleDriveFolder property, hence the RaisePropertyChanged(); is never called.
**Edit 3: **
As of your code from that tutorial, your code isn't exactly following the tutorial. The guy in the tutorial is binding to the property NotifyTaskCompletion. You are binding it to the backing field though.
public MainWindowViewModel()
{
var service = DriveHelper.createDriveService("client_secret.json", false)
// your property is named googleDriveFolders, but you are assigning it to _googleDriveFolders
googleDriveFolders = new NotifyTaskCompletion<List<File>>( DriveHelper.getFiles(service), "trashed=false and mimeType = 'application/vnd.google-apps.folder'"));
}
This code, when complete, won't call RaisePropertyChanged("googleDriveFolder") (which is your observable list), because NotifyTaskCompletion will only refresh it's own property. It's very likely you have bounded your View to the googleDriveFolder (Observable property) rather than to googleDriveFolders.Result.
For this example it's imperative to bind to googleDriveFolders.Result, because the change notification will only get fired for the Result Property of the NotificationTaskCompletition object as seen in the examples code propertyChanged(this, new PropertyChangedEventArgs("Result"));.
So your XAML has to look something like
<ListView Source="{Binding googleDriveFolders.Result}"/>
But anyhows, the issue still remains, that it's bad practice to do async operations within the constructor, so even within your Unit Tests for example, it would start the async task everytime the object is initialized, so in every UnitTest even if you test different stuff and you can't pass parameters to it easily (like passing a link or a folder name which to load).
So the clean way is doing it via navigation service and an INavigationAware implementation for ViewModels that require it (modes that do not do any async operation just don't implement this interface).
I came to this solution ... But I think this isn't the best way. <.<
Using a Listview.
namespace UpdateUploader.ViewModels
{
using System.Windows.Input;
using Helper;
using Services;
using System.Collections;
using System.Collections.ObjectModel;
using Google.Apis.Drive.v2.Data;
using Google.Apis.Drive.v2;
using System.Collections.Generic;
public class MainWindowViewModel : ViewModelBase
{
DriveService _service;
public MainWindowViewModel()
{
_service = DriveHelper.createDriveService("client_secret.json", false);
googleDriveFolders = new NotifyTaskCompletion<List<File>>( DriveHelper.getFiles(_service, "trashed=false and mimeType = 'application/vnd.google-apps.folder'"));
}
public NotifyTaskCompletion<List<File>> _googleDriveFolders;
public NotifyTaskCompletion<List<File>> googleDriveFolders
{
get { return _googleDriveFolders; }
set
{
_googleDriveFolders = value;
RaisePropertyChanged();
}
}
#region ICommands
private ICommand _refreshFoldersCommand;
public ICommand refreshFoldersCommand
{
get
{
if (this._refreshFoldersCommand == null)
{
_refreshFoldersCommand = new RelayCommand(p => this.loadFolders(p));
}
return this._refreshFoldersCommand;
}
}
#endregion ICommands
public void loadFolders(object parameter)
{
googleDriveFolders = new NotifyTaskCompletion<List<File>>(DriveHelper.getFiles(_service, "trashed=false and mimeType = 'application/vnd.google-apps.folder'"));
}
}
}
EDIT
NotifyTaskCompletion.cs
namespace UpdateUploader.Helper
{
using System;
using System.ComponentModel;
using System.Threading.Tasks;
public sealed class NotifyTaskCompletion<TResult> : INotifyPropertyChanged
{
public NotifyTaskCompletion(System.Threading.Tasks.Task<TResult> task)
{
Task = task;
if (!task.IsCompleted)
{
var _ = WatchTaskAsync(task);
}
}
private async Task WatchTaskAsync(Task task)
{
try
{
await task;
}
catch
{
}
var propertyChanged = PropertyChanged;
if (propertyChanged == null)
return;
propertyChanged(this, new PropertyChangedEventArgs("Status"));
propertyChanged(this, new PropertyChangedEventArgs("IsCompleted"));
propertyChanged(this, new PropertyChangedEventArgs("IsNotCompleted"));
if (task.IsCanceled)
{
propertyChanged(this, new PropertyChangedEventArgs("IsCanceled"));
}
else if (task.IsFaulted)
{
propertyChanged(this, new PropertyChangedEventArgs("IsFaulted"));
propertyChanged(this, new PropertyChangedEventArgs("Exception"));
propertyChanged(this, new PropertyChangedEventArgs("InnerException"));
propertyChanged(this, new PropertyChangedEventArgs("ErrorMessage"));
}
else
{
propertyChanged(this, new PropertyChangedEventArgs("IsSuccessfullyCompleted"));
propertyChanged(this, new PropertyChangedEventArgs("Result"));
}
}
public Task<TResult> Task { get; private set; }
public TResult Result
{
get { return (Task.Status == TaskStatus.RanToCompletion) ? Task.Result : default(TResult); }
}
public TaskStatus Status { get { return Task.Status; } }
public bool IsCompleted { get { return Task.IsCompleted; } }
public bool IsNotCompleted { get { return !Task.IsCompleted; } }
public bool IsSuccessfullyCompleted
{
get
{
return Task.Status == TaskStatus.RanToCompletion;
}
}
public bool IsCanceled { get { return Task.IsCanceled; } }
public bool IsFaulted { get { return Task.IsFaulted; } }
public AggregateException Exception { get { return Task.Exception; } }
public Exception InnerException
{
get
{
return (Exception == null) ? null : Exception.InnerException;
}
}
public string ErrorMessage
{
get
{
return (InnerException == null) ? null : InnerException.Message;
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
}
DriveService.cs(excerpt; createDriveService, getfiles)
namespace UpdateUploader.Services
{
using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Google.Apis.Auth.OAuth2;
using Google.Apis.Drive.v2;
using Google.Apis.Drive.v2.Data;
using Google.Apis.Services;
using Google.Apis.Util.Store;
using Google.Apis.Upload;
class DriveHelper
{
private static bool _unique;
public static DriveService createDriveService(string passFilePath, bool createUniqueID)
{
_unique = createUniqueID;
if (!System.IO.File.Exists(passFilePath))
{
Console.Error.WriteLine("keyfile not found...");
return null;
}
string[] scopes = new string[] { DriveService.Scope.Drive }; // Full accces
// loading the key file
UserCredential credential;
using (var stream = new System.IO.FileStream("client_secret.json", System.IO.FileMode.Open, System.IO.FileAccess.Read))
{
string credPath = System.Environment.GetFolderPath(
System.Environment.SpecialFolder.Personal);
credPath = System.IO.Path.Combine(credPath, ".credentials/update-uploader");
credential = GoogleWebAuthorizationBroker.AuthorizeAsync(
GoogleClientSecrets.Load(stream).Secrets,
scopes,
"user",
CancellationToken.None,
new FileDataStore(credPath, true)).Result;
Console.WriteLine("Credential file saved to: " + credPath);
}
var service = new DriveService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = "Update Uploader",
});
return service;
}
// search = null ; get all files/folders
public static async Task<List<File>> getFiles(DriveService service, string search)
{
System.Collections.Generic.List<File> Files = new System.Collections.Generic.List<File>();
try
{
// list all files with max 1000 results
FilesResource.ListRequest list = service.Files.List();
list.MaxResults = 1000;
if (search != null)
{
list.Q = search;
}
FileList filesFeed = await list.ExecuteAsync();
while (filesFeed.Items != null)
{
foreach (File item in filesFeed.Items)
{
Files.Add(item);
}
// if it is the last page break
if (filesFeed.NextPageToken == null)
{
break;
}
list.PageToken = filesFeed.NextPageToken;
filesFeed = list.Execute();
}
}
catch (Exception ex)
{
Console.Error.WriteLine(ex.Message);
}
return Files;
}
ViewModelBase.cs
namespace UpdateUploader.Helper
{
using System.ComponentModel;
using System.Runtime.CompilerServices;
public abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged([CallerMemberName] string propertyName = null)
{
// new since 4.6 or 4.5
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}

Binding a Grouped Table View using Xamarin.iOS and MvvmCross

I am trying to create a Grouped Table View using Xamarin.iOS and MvvmCross. But can't get the binding part right, my ItemSource is always null and RaisePropertyChanged in the ViewModel won't trigger the the setter of my ItemsSource. I have looked at the conference sample Stuart refers to in Creating UITable with section using Mono touch and slodge mvvmcross, but I am currently stuck where I am.
This is the TableSource I am using:
public class VoiceMessagesTableSource : MvxTableViewSource
{
private List<VoiceMessagesTableItemGroup> _itemsSource;
private const string _cellNibName = "MessagesTableCell";
private static readonly NSString _cellIdentifier = new NSString(_cellNibName);
public VoiceMessagesTableSource(UITableView tableView) : base(tableView)
{
tableView.RegisterNibForCellReuse(UINib.FromName(_cellNibName, NSBundle.MainBundle), _cellIdentifier);
}
public List<VoiceMessagesTableItemGroup> ItemsSource
{
get
{
return _itemsSource;
}
set
{
_itemsSource = value;
ReloadTableData();
}
}
public override int NumberOfSections(UITableView tableView)
{
if (_itemsSource == null)
{
return 0;
}
return _itemsSource.Count;
}
public override int RowsInSection(UITableView tableview, int section)
{
if (_itemsSource == null)
{
return 0;
}
return _itemsSource[section].Count;
}
protected override UITableViewCell GetOrCreateCellFor(UITableView tableView, NSIndexPath indexPath, object item)
{
return (UITableViewCell)TableView.DequeueReusableCell(_cellIdentifier, indexPath);
}
protected override object GetItemAt(NSIndexPath indexPath)
{
if (_itemsSource == null)
{
return null;
}
return _itemsSource[indexPath.Section][indexPath.Row];
}
}
This is how I bind it in the view:
private void BindMessagesTable()
{
var data_source = new VoiceMessagesTableSource(MessagesTable);
MvxFluentBindingDescriptionSet<MessagesView, MessagesViewModel> set = this.CreateBindingSet<MessagesView, MessagesViewModel>();
//set.Bind(data_source).To(viewModel => viewModel.GroupedVoiceMessages);
set.Bind(data_source.ItemsSource).To(viewModel => viewModel.GroupedVoiceMessages);
set.Bind(data_source).For(s => s.SelectionChangedCommand).To(viewModel => viewModel.DisplayVoiceMessageCommand);
set.Apply();
MessagesTable.Source = data_source;
MessagesTable.ReloadData();
}
And this is the ItemGroup class I am using as my items source:
public class VoiceMessagesTableItemGroup : List<MessageEntryViewModel>
{
public VoiceMessagesTableItemGroup(string key, IEnumerable<MessageEntryViewModel> items) : base(items)
{
Key = key;
}
public string Key { get; set; }
}
I've not analysed all the code in the question, but one issue I can see is that it looks like you are trying to Bind the wrong target.
When you write:
set.Bind(foo).To(viewModel => viewModel.Bar);
then this tries to bind the default binding property on foo to the ViewModel's Bar property.
When you write:
set.Bind(foo).For(view => view.Zippy).To(viewModel => viewModel.Bar);
then this tries to bind the Zippy property on foo to the ViewModel's Bar property.
So, your line:
set.Bind(data_source.ItemsSource).To(viewModel => viewModel.GroupedVoiceMessages);
tries to bind an unknown property on the null ItemsSource to GroupedVoiceMessages
This obviously won't work - and there should be some warnings shown about the null in the trace.
Instead try:
set.Bind(data_source).For(v => v.ItemsSource).To(viewModel => viewModel.GroupedVoiceMessages);

How to bind a command inside a View to a ViewModel in MvvmCross

I have this android View, where I'm using LegacyBar:
public class BaseView : MvxActivity
{
public override void SetContentView(int layoutResId)
{
base.SetContentView(layoutResId);
var bar = FindViewById<LB.LegacyBar>(Resource.Id.actionbar);
bar.SetHomeLogo(Resource.Drawable.Icon);
var attr = this.GetType()
.GetCustomAttributes(typeof(ActivityAttribute), true)
.FirstOrDefault() as ActivityAttribute;
if (attr != null)
bar.Title = attr.Label;
bar.SetHomeAction(new ActionLegacyBarAction(ApplicationContext, doHomeAction, Resource.Drawable.Icon));
}
public ICommand homeActionClicked { get; set; }
private void doHomeAction()
{
if (homeActionClicked != null && homeActionClicked.CanExecute(null))
homeActionClicked.Execute(null);
}
}
What is the right way to bind homeActionClicked defined inside this View to a Command in its ViewModel?
Or maybe I should make a custom control and put the bar inside it and then bind the control's command?
You can reference your ViewModel as a property in your view so you can access it's property's and set a command from your view to the viewmodel.
protected MvvMCore.Core.ViewModels.NavigationBarViewModel NavigationBarViewModel {
get{ return base.ViewModel as MvvMCore.Core.ViewModels.NavigationBarViewModel; }
}
For this property, you can do as #Chris suggests - using base.ViewModel and casting to give you access to the ViewModel, or you can use FluentBinding if you want to do binding inside OnCreate - e.g.
var set = this.CreateBindingSet<BaseView, BaseViewModel>();
set.Bind(this).For(v => v.homeActionClicked).To(vm => vm.GoHomeCommand);
set.Apply();

Markup extension in XAML for binding to ISubject<string>

If I have the following view model
class Foo : INotifyPropertyChanged {
ISubject<string> Name { ... }
}
and some imagined XAML code
<TextBox Text="{my:Subscribe Path=Name}/>
I wish the two way binding to behave that
Subject.onNext is called when the text box is updated in the UI
the text box is updated by subscribing to the Subject.Subscribe
As WPF only supports INPC directly my idea is to create a proxy INPC object
in via a markup extension
class WPFSubjectProxy : INotifyPropertyChanged{
string Value { ... }
}
The proxy would be wired up to the subject as so
subject.Subscribe(v=>proxy.Value=v);
proxy
.WhenAny(p=>p.Value, p.Value)
.Subscribe(v=>subject.OnNext(v))
Note WhenAny is a ReactiveUI helper for subscribing to
INPC events.
But then I would need to generate a binding and return
that via the markup extension.
I know what I want to do but can't figure out the
Markup extension magic to put it all together.
It's hard to say without seeing specifically what you're struggling with, but perhaps this helps?
EDIT
The solution I (bradgonesurfing) came up with is below thanks to the pointer in the
assigned correct answer.
    Nodes
     
and the implementing code. It has a dependency on ReactiveUI and a helper function in a private library for binding ISubject to a mutable property on an INPC supporting object
using ReactiveUI.Subjects;
using System;
using System.Linq;
using System.Reactive.Subjects;
using System.Windows;
using System.Windows.Data;
using System.Windows.Markup;
namespace ReactiveUI.Markup
{
[MarkupExtensionReturnType(typeof(BindingExpression))]
public class SubscriptionExtension : MarkupExtension
{
[ConstructorArgument("path")]
public PropertyPath Path { get; set; }
public SubscriptionExtension() { }
public SubscriptionExtension(PropertyPath path)
{
Path = path;
}
class Proxy : ReactiveObject
{
string _Value;
public string Value
{
get { return _Value; }
set { this.RaiseAndSetIfChanged(value); }
}
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
var pvt = serviceProvider as IProvideValueTarget;
if (pvt == null)
{
return null;
}
var frameworkElement = pvt.TargetObject as FrameworkElement;
if (frameworkElement == null)
{
return this;
}
object propValue = GetProperty(frameworkElement.DataContext, Path.Path);
var subject = propValue as ISubject<string>;
var proxy = new Proxy();
Binding binding = new Binding()
{
Source = proxy,
Path = new System.Windows.PropertyPath("Value")
};
// Bind the subject to the property via a helper ( in private library )
var subscription = subject.ToMutableProperty(proxy, x => x.Value);
// Make sure we don't leak subscriptions
frameworkElement.Unloaded += (e,v) => subscription.Dispose();
return binding.ProvideValue(serviceProvider);
}
private static object GetProperty(object context, string propPath)
{
object propValue = propPath
.Split('.')
.Aggregate(context, (value, name)
=> value.GetType()
.GetProperty(name)
.GetValue(value, null));
return propValue;
}
}
}

Caliburn Micro: how to navigate in Windows phone silverlight

i am trying to use Caliburn Micro in my windows phone 7 project.
But i got a nullreferenceexception when navigate the page.
namespace Caliburn.Micro.HelloWP7 {
public class MainPageViewModel {
readonly INavigationService navigationService;
public MainPageViewModel(INavigationService navigationService) {
this.navigationService = navigationService;
}
public void GotoPageTwo() {
/*navigationService.UriFor<PivotPageViewModel>()
.WithParam(x => x.NumberOfTabs, 5)
.Navigate();*/
navigationService.UriFor<Page1ViewModel>().Navigate();
}
}
}
namespace Caliburn.Micro.HelloWP7
{
public class Page1ViewModel
{
readonly INavigationService navigationService;
public Page1ViewModel(INavigationService navigationService)
{
this.navigationService = navigationService;
}
}
}
can anyone tell me what's the problem of my code? thanks in advance.
here is bootstrapper:
public class ScheduleBootstrapper : PhoneBootstrapper
{
PhoneContainer container;
protected override void Configure()
{
container = new PhoneContainer(RootFrame);
container.RegisterPhoneServices();
container.PerRequest<MainPageViewModel>();
container.PerRequest<MainContentViewModel>();
container.PerRequest<Page1ViewModel>();
AddCustomConventions();
}
static void AddCustomConventions()
{
ConventionManager.AddElementConvention<Pivot>(Pivot.ItemsSourceProperty, "SelectedItem", "SelectionChanged").ApplyBinding =
(viewModelType, path, property, element, convention) =>
{
if (ConventionManager
.GetElementConvention(typeof(ItemsControl))
.ApplyBinding(viewModelType, path, property, element, convention))
{
ConventionManager
.ConfigureSelectedItem(element, Pivot.SelectedItemProperty, viewModelType, path);
ConventionManager
.ApplyHeaderTemplate(element, Pivot.HeaderTemplateProperty, viewModelType);
return true;
}
return false;
};
ConventionManager.AddElementConvention<Panorama>(Panorama.ItemsSourceProperty, "SelectedItem", "SelectionChanged").ApplyBinding =
(viewModelType, path, property, element, convention) =>
{
if (ConventionManager
.GetElementConvention(typeof(ItemsControl))
.ApplyBinding(viewModelType, path, property, element, convention))
{
ConventionManager
.ConfigureSelectedItem(element, Panorama.SelectedItemProperty, viewModelType, path);
ConventionManager
.ApplyHeaderTemplate(element, Panorama.HeaderTemplateProperty, viewModelType);
return true;
}
return false;
};
}
protected override object GetInstance(Type service, string key)
{
return container.GetInstance(service, key);
}
protected override IEnumerable<object> GetAllInstances(Type service)
{
return container.GetAllInstances(service);
}
protected override void BuildUp(object instance)
{
container.BuildUp(instance);
}
}
I had this too, and tracked it down as follows:
As you know, Caliburn.Micro uses convention-over-configuration to locate Views for ViewModels, and vice-versa, which means we need to follow the conventions. My mistake was to have the namespace's inconsistent for the View and ViewModel
In my case, I had
MyWP7App.DetailsViewModel, and
MyWP7App.Views.DetailsView
--> I renamed the VM's namespace to be MyWP7App.ViewModels.DetailsViewModel, and it worked out fine. I think I could have moved the view into MyWP7App.DetailsView for a good result, too...
Under the covers
the call to Navigate() invokes DeterminePageName() which, in turn, invokes ViewLocator.LocateTypeForModelType
This, like the rest of CM is overridable, but the default implementation looks like this:
public static Func<Type, DependencyObject, object, Type> LocateTypeForModelType = (modelType, displayLocation, context) => {
var viewTypeName = modelType.FullName.Substring(
0,
modelType.FullName.IndexOf("`") < 0
? modelType.FullName.Length
: modelType.FullName.IndexOf("`")
);
Func<string, string> getReplaceString;
if (context == null) {
getReplaceString = r => { return r; };
}
else {
getReplaceString = r => {
return Regex.Replace(r, Regex.IsMatch(r, "Page$") ? "Page$" : "View$", ContextSeparator + context);
};
}
var viewTypeList = NameTransformer.Transform(viewTypeName, getReplaceString);
var viewType = (from assembly in AssemblySource.Instance
from type in assembly.GetExportedTypes()
where viewTypeList.Contains(type.FullName)
select type).FirstOrDefault();
return viewType;
};
If you follow the debugger through, you end up with a collection viewTypeList that contains MyWP7App.DetailsView, and a type whose full name is MyWP7App.Views.DetailsView, and the viewType returned is therefore null... this is the cause of the NullReferenceException.
I'm 99% sure the NameTransformer.Transform call will perform a pattern-match and transform the ViewModels in the namespace of the VM to Views in the namespace of the View it's trying to locate...

Categories

Resources