MvvmCross no bindings created - xamarin

I just upgraded a project from an older version of MvvmCross to the latest version, and I'm having trouble with bindings.
I'm aware of the LinkerPleaseInclude hack, and it looks like (at least some of) the properties I'm using are listed there.
I'm concerned about my usage of the Views and ViewModels. Here's an example.
public partial class HomeView : MvxViewController<HomeViewModel>
...
public override void ViewDidLoad ()
{
base.ViewDidLoad();
this.CreateBinding(BudgetTrackerButton).To((HomeViewModel vm) => vm.BudgetTracker).Apply();
this.CreateBinding(LoginButton).For("Title").To ((HomeViewModel vm) => vm.LogMessage).Apply();
this.CreateBinding(LoginButton).To((HomeViewModel vm) => vm.Login).Apply();
this.CreateBinding(ContactApprisenButton).To((HomeViewModel vm) => vm.Contact).Apply();
this.CreateBinding(AboutApprisenButton).To((HomeViewModel vm) => vm.About).Apply();
this.CreateBinding(AboutThisAppButton).To((HomeViewModel vm) => vm.AboutApp).Apply();
this.CreateBinding(FAQsButton).To((HomeViewModel vm) => vm.FAQs).Apply();
this.CreateBinding(PrivacyPolicyButton).To((HomeViewModel vm) => vm.PrivacyPolicy).Apply();
}
public abstract class MvxViewController<T> : UIViewController, IMvxBindingContextOwner, IUIWrappable, MvvmCross.iOS.Views.IMvxIosView where T : ViewModelBase
...
}
protected MvvmCross.Core.ViewModels.IMvxViewModel _viewModel = null;
public MvvmCross.Core.ViewModels.IMvxViewModel ViewModel {
get {
if (_viewModel == null) {
_viewModel = MvvmCross.Platform.Mvx.Resolve<T> ();
}
return _viewModel;
}
set {
_viewModel = value;
}
}
public MvvmCross.Core.ViewModels.MvxViewModelRequest Request {
get;
set;
}
public object DataContext {
get {
return _viewModel;
}
set {
_viewModel = (MvvmCross.Core.ViewModels.IMvxViewModel)value;
}
}
protected MvvmCross.Binding.BindingContext.IMvxBindingContext _bindingContext;
public IMvxBindingContext BindingContext {
get {
if (_bindingContext == null) {
_bindingContext = new MvvmCross.Binding.BindingContext.MvxBindingContext ();
}
return _bindingContext;
}
set {
_bindingContext = value;
}
}
CreateBinding is from the MvxBindingContextOwnerExtensions.
When I hit CreateBinding, the view model has been made.
I understand DataContext to be the same as the ViewModel, and I only include it to conform to the MvvmCross.iOS.Views.IMvxIosView interface.
Am I missing a step somewhere? An interface?
Matching views to view models should happen automatically based on naming conventions, right? (They didn't.. in my case, I had to manually specify the mappings in my Setup class. Could be contributing to this problem.)
Strangely enough, this works (for the button title anyway, I haven't tested the other bindings, and I'm not interested in updating all the bindings across the entire application if the fix is simple):
var set = this.CreateBindingSet<HomeView, HomeViewModel>();
set.Bind(LoginButton).For("Title").To(vm => vm.LogMessage);
I can post more code if something else would be relevant. I'm also new to MvvmCross.

Related

Automatically switching views for AMP in ASP.NET MVC

I want to create and AMP version of my website in ASP.NET MVC using .NET Core 2.0. Previously I had done some work with DisplayModeProvider instances in tha past on .Net framework, but that does not seem to be an option in .NET Core.
What I want to be able to do is alter the view names to be index.amp.cshtml rather than index.cshtml when my URL starts iwth /amp. What's the best way to achieve this in .NET Core?
You can do something like this using IViewLocationExpander. As it happens, I was playing with this a few days ago so I have some code samples to hand. If you create something like this:
public class AmpViewLocationExpander : IViewLocationExpander
{
public void PopulateValues(ViewLocationExpanderContext context)
{
var contains = context.ActionContext.HttpContext.Request.Query.ContainsKey("amp");
context.Values.Add("AmpKey", contains.ToString());
var containsStem = context.ActionContext.HttpContext.Request.Path.StartsWithSegments("/amp");
context.Values.Add("AmpStem", containsStem.ToString());
}
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
{
if (!(context.ActionContext.ActionDescriptor is ControllerActionDescriptor descriptor)) { return viewLocations; }
if (context.ActionContext.HttpContext.Request.Query.ContainsKey("amp")
|| context.ActionContext.HttpContext.Request.Path.StartsWithSegments("/amp")
)
{
return viewLocations.Select(x => x.Replace("{0}", "{0}.amp"));
}
return viewLocations;
}
}
iViewLocationExpander can be found in Microsoft.AspNetCore.Mvc.Razor
Then in your Configure Services method in Startup.cs, add the following:
services.Configure<RazorViewEngineOptions>(options =>
{
options.ViewLocationExpanders.Add(new AmpViewLocationExtender());
});
What this will do is update the view locations per request to insert .amp before .cshtml any time the URL either starts with /amp or there is a query string key of amp. If your AMP views don't exist, it might blow-up a little, I've not fully tested it, but it should get you started.
You can define this Middleware :
public class AmpMiddleware
{
private RequestDelegate _next;
public AmpMiddleware(RequestDelegate next)
{
_next = next;
}
public Task Invoke(HttpContext context)
{
const string ampTag = "/amp";
var path = context.Request.Path;
if (path.HasValue)
{
var ampPos = path.Value.IndexOf(ampTag);
if (ampPos >= 0)
{
context.Request.Path = new PathString(path.Value.Remove(ampPos, ampTag.Length));
context.Items.Add("amp", "true");
}
}
return _next(context);
}
}
public static class BuilderExtensions
{
public static IApplicationBuilder UseAmpMiddleware(this IApplicationBuilder app)
{
return app.UseMiddleware<AmpMiddleware>();
}
}
And call it in Startup:
app.UseAmpMiddleware();
Then can check in page and simple set another layout or limit some code, in his way no need to create separate page for amp version:
#if (HttpContext.Items.ContainsKey("amp"))
{
<b>Request AMP Version</b>
}

Dynamic ViewModel navigation

I am trying to find a way to be able to set from the View to what ViewModel I have to navigate. This is to be able to change the navigation flow without changing the core project.
I thought the easier way would be creating an interface, setting the target ViewModel there and injecting the interface into the ViewModel to then perform the navigation.
public interface IModelMapping
{
MvxViewModel ViewModelToNavigate();
}
public class MyViewModel : MvxViewModel
{
readonly IMvxNavigationService navigationService;
readonly IModelMapping modelMapping;
public MyViewModel(IMvxNavigationService navigationService, IModelMapping modelMapping)
{
this.navigationService = navigationService;
this.modelMapping = modelMapping;
}
public IMvxAsyncCommand GoContent
{
get
{
IMvxViewModel vm = modelMapping.ViewModelToNavigate();
IMvxAsyncCommand navigateCommand = new MvxAsyncCommand(() => navigationService.Navigate<vm>());
return navigteCommand;
}
}
}
The problem with this code is I am getting an error setting the navigationService.Navigate(). The error is 'vm is a variable but it is used like a type'
What about using the URI navigation together with the facade? See also https://www.mvvmcross.com/documentation/fundamentals/navigation#uri-navigation
Say you are building a task app and depending on the type of task you want to show a different view. This is where NavigationFacades come in handy (there is only so much regular expressions can do for you).
mvx://task/?id=00 <– this task is done, show read-only view (ViewModelA)
mvx://task/?id=01 <– this task isn’t, go straight to edit view (ViewModelB)
[assembly: MvxRouting(typeof(SimpleNavigationFacade), #"mvx://task/\?id=(?<id>[A-Z0-9]{32})$")]
namespace *.NavigationFacades
{
public class SimpleNavigationFacade
: IMvxNavigationFacade
{
public Task<MvxViewModelRequest> BuildViewModelRequest(string url,
IDictionary<string, string> currentParameters, MvxRequestedBy requestedBy)
{
// you can load data from a database etc.
// try not to do a lot of work here, as the user is waiting for the UI to do something ;)
var viewModelType = currentParameters["id"] == Guid.Empty.ToString("N") ? typeof(ViewModelA) : typeof(ViewModelB);
return Task.FromResult(new MvxViewModelRequest(viewModelType, new MvxBundle(), null, requestedBy));
}
}
}

Set different binding context for custom user control

I am trying to create my first Xamarin.Forms custom user control named LocationSelector. It has an Entry and when the user enters something, a list with selectable locations is shown (similary to the selection in Google Maps).
The selected location is the important 'return value' of the control.
My plan is to catch the ItemSelected event of the list and set the SelectedLocation property. LocationSelector is designed as MVVM and since everything is working so far here just the Code-Behind (which I think is enough to describe the problem):
public partial class LocationSelector : StackLayout
{
public static readonly BindableProperty SelectedLocationProperty =
BindableProperty.Create<LocationSelector, LocationModel>(s => s.SelectedLocation, new LocationModel(), BindingMode.TwoWay);
public LocationSelector()
{
InitializeComponent();
var model = new LocationSelectorModel();
BindingContext = model;
_listView.ItemSelected += (sender, args) =>
{
SelectedLocation = model.SelectedLocation;
};
}
public LocationModel SelectedLocation
{
get { return (LocationModel)GetValue(SelectedLocationProperty); }
set { SetValue(SelectedLocationProperty, value); }
}
}
Now I want to use this control on a search view where the BindingContext is set to the SearchViewModel:
<ContentPage x:Class="Application.App.Views.SearchView" ...>
<c:LocationSelector SelectedLocation="{Binding Location}"/>
</ContentPage>
public class SearchViewModel : ViewModel
{
private LocationModel _location;
public LocationModel Location
{
get { return _location; }
set { SetProperty(ref _location, value); }
}
}
Unfortunately this is not working. The output throws a binding warning:
Binding: 'Location' property not found on 'Application.App.CustomControls.LocationSelectorModel', target property: 'Application.App.CustomControls.LocationSelector.SelectedLocation'
Why points the binding to a property in the ViewModel that is used 'within' my custom control and not to the property in the BindingContext of the view?
The problem is setting the BindingContext to the view model of the user control:
public LocationSelector()
{
var model = new LocationSelectorModel();
BindingContext = model; // this causes the problem
// ...
}
In this post I found the solution. Setting the BindingContext to each child elements rather than the whole user control is doing the job:
public LocationSelector()
{
var model = new LocationSelectorModel();
foreach (var child in Children)
{
child.BindingContext = model;
}
}

Is there a way to invalidate the ASP.NET MVC 3 ActionCache

I have an ASP.NET MVC 3 application that uses custom attributes to create select controls for model properties that can be populated from external data sources at runtime. The issue is that my EditorTemplate output appear to be cached at the application level, so my drop down lists are not updated when their data source changes until the Application Pool is recycled.
I also have output the contents of the MVC 3 ActionCache that is bound to the ViewContext.HttpContext object as shown in the MVC 3 source code in System.Web.Mvc.Html.TemplateHelpers.cs:95.
Action Cache GUID: adf284af-01f1-46c8-ba15-ca2387aaa8c4:
Action Cache Collection Type: System.Collections.Generic.Dictionary``2[System.String,System.Web.Mvc.Html.TemplateHelpers+ActionCacheItem]
Action Cache Dictionary Keys: EditorTemplates/Select
So it appears that the Select editor template is definitely being cached, which would result in the TemplateHelper.ExecuteTemplate method to always return the cached value instead of calling ViewEngineResult.View.Render a second time.
Is there any way to clear the MVC ActionCache or otherwise force the Razor view engine to always re-render certain templates?
For reference, Here are the relevant framework components:
public interface ISelectProvider
{
IEnumerable<SelectListItem> GetSelectList();
}
public class SelectAttribute : Attribute, IMetadataAware
{
private readonly ISelectProvider _provider;
public SelectAttribute(Type type)
{
_provider = DependencyResolver.Current.GetService(type) as ISelectProvider;
}
public void OnMetadataCreated(ModelMetadata modelMetadata)
{
modelMetadata.TemplateHint = "Select";
modelMetadata.AdditionalValues.Add("SelectListItems", SelectList);
}
public IEnumerable<SelectListItem> SelectList
{
get
{
return _provider.GetSelectList();
}
}
}
Next, there is a custom editor template in ~\Views\Shared\EditorTemplates\Select.cshtml.
#model object
#{
var selectList = (IEnumerable<SelectListItem>)ViewData.ModelMetadata.AdditionalValues["SelectListItems"];
foreach (var item in selectList)
{
item.Selected = (item != null && Model != null && item.Value.ToString() == Model.ToString());
}
}
#Html.DropDownListFor(s => s, selectList)
Finally, I have a view model, select provider class and a simple view.
/** Providers/MySelectProvider.cs **/
public class MySelectProvider : ISelectProvider
{
public IEnumerable<SelectListItem> GetSelectList()
{
foreach (var item in System.IO.File.ReadAllLines(#"C:\Test.txt"))
{
yield return new SelectListItem() { Text = item, Value = item };
}
}
}
/** Models/ViewModel.cs **/
public class ViewModel
{
[Select(typeof(MySelectProvider))]
public string MyProperty { get; set; }
}
/** Views/Controller/MyView.cshtml **/
#model ViewModel
#using (Html.BeginForm())
{
#Html.EditorForModel()
<input type="submit" value="Submit" />
}
** EDIT **
Based on suggestions in the comment, I started to look more closely at the ObjectContext lifecycle. While there were some minor issues, the issue appears to be isolated to an odd behavior involving a callback within a LINQ expression in the SelectProvider implementation.
Here is the relevant code.
public abstract class SelectProvider<R, T> : ISelectProvider
where R : class, IQueryableRepository<T>
{
protected readonly R repository;
public SelectProvider(R repository)
{
this.repository = repository;
}
public virtual IEnumerable<SelectListItem> GetSelectList(Func<T, SelectListItem> func, Func<T, bool> predicate)
{
var ret = new List<SelectListItem>();
foreach (T entity in repository.Table.Where(predicate).ToList())
{
ret.Add(func(entity));
}
return ret;
}
public abstract IEnumerable<SelectListItem> GetSelectList();
}
public class PrinterSelectProvider : SelectProvider<IMyRepository, MyEntityItem>
{
public PrinterSelectProvider()
: base(DependencyResolver.Current.GetService<IMyRepository>())
{
}
public override IEnumerable<SelectListItem> GetSelectList()
{
// Create a sorted list of items (this returns stale data)
var allItems = GetSelectList(
x => new SelectListItem()
{
Text = x.DisplayName,
Value = x.Id.ToString()
},
x => x.Enabled
).OrderBy(x => x.Text);
// Do the same query, but without the callback
var otherItems = repository.Table.Where(x => x.Enabled).ToList().Select(x => new SelectListItem()
{
Text = x.DisplayName,
Value = x.Id.ToString()
}).OrderBy(x => x.Text);
System.Diagnostics.Trace.WriteLine(string.Format("Query 1: {0} items", allItems.Count()));
System.Diagnostics.Trace.WriteLine(string.Format("Query 2: {0} items", otherItems.Count()));
return allItems;
}
}
And, the captured output from the System.Diagnostics.Trace is
Query 1: 2 items
Query 2: 3 items
I'm not sure what could be going wrong here. I considered that the Select may need an Expressions, but I just double-checked and the LINQ Select method only takes Func objects.
Any additional suggetions?
Problem Solved!
I finally had a chance to re-visit this issue. The root cause had nothing to do with LINQ, the ActionCache, or the ObjectContext, rather it was related to when attribute constructors are called.
As shown, my custom SelectAttribute class calls DependencyResolver.Current.GetService in its constructor to create an instance of the ISelectProvider class. However, the ASP.NET MVC framework scans the assemblies for custom metadata attributes once and keeps a reference to them in the application scope. As explained in the linked question, accessing a Attribute triggers its constructor.
So, the constructor was run only once, rather than on each request, as I had assumed. This meant that there was actually only one, cached instance of the PrinterSelectProvider class instantiated that was shared across all requests.
I solved the problem by changing the SelectAttribute class like this:
public class SelectAttribute : Attribute, IMetadataAware
{
private readonly Type type;
public SelectAttribute(Type type)
{
this.type = type;
}
public void OnMetadataCreated(ModelMetadata metadata)
{
// Instantiate the select provider on-demand
ISelectProvider provider = DependencyResolver.Current.GetService(type) as ISelectProvider;
modelMetadata.TemplateHint = "Select";
modelMetadata.AdditionalValues.Add("SelectListItems", provider.GetSelectList());
}
}
Tricky problem indeed!

asp.net mvc ObjectDisposedException with ef

I need some help with this error "The ObjectContext instance has been disposed and can no longer be used for operations that require a connection."
It's a asp.net mvc3, EF4, and ms sql.
Here is the razor with two dropdowns:
<div class="editRow">
#Html.DropDownListFor(m=>m.IndustryId, (SelectList)ViewBag.Industry, #Empower.Resource.General.ddlDefaultVal, new { #class = "ddl400" })
#Html.ValidationMessageFor(m => m.IndustryId)
</div>
<div class="editRow">
#Html.DropDownListFor(m=>m.ProvinceId, (SelectList)ViewBag.Province, #Empower.Resource.General.ddlDefaultVal, new {#class = "ddl400"})
#Html.ValidationMessageFor(m => m.ProvinceId)
</div>
Controller:
IndustryService indService = new IndustryService();
ViewBag.Industry = new SelectList(indService.GetAllIndustry(), "IndustryId", "IndustryName");
ProvinceService proService = new ProvinceService();
ViewBag.Province = new SelectList(proService.GetAllProvince(), "ProvinceId", "ProvinceName");
return View();
ProvinceService:
public IEnumerable<Province> GetAllProvince()
{
using (var context = DBContext.ObjectContext)
{
var pros = context.Provinces;
return pros;
}
}
IndustryService is identical as above...
public class DBContext
{
private static EmpowerDBEntities _empowerContext;
public static EmpowerDBEntities ObjectContext
{
get
{
if (_empowerContext == null)
_empowerContext = new EmpowerDBEntities();
return _empowerContext;
}
}
}
I know the problem occurs in second dropdown when it tries to retrive data while the connection is desposed by previous query. Please help me with this, thanks.
The fix is simple - convert to a .ToList() or First() before using. LINQ has deferred execution and tries to run this command after the context is disposed (when your object results are referenced) - not when you actually make the call.. You need to force it to run now while the context is in scope.
using (var context = DBContext.ObjectContext)
{
var pros = context.Provinces;
return pros.ToList();
}
Also - your code above is checking for null in the get accessor. However this object won't be null - it will be disposed, so you cannot do your check this way, you need to check if its null and not disposed.
public class DBContext
{
private static EmpowerDBEntities _empowerContext;
public static EmpowerDBEntities ObjectContext
{
get
{
if (_empowerContext == null || _empowerContext.IsDisposed())
_empowerContext = new EmpowerDBEntities();
return _empowerContext;
}
}
}
something like that anyways :)
I ran into a similar problem. I had been following this pattern, which I had seen in many code examples on the web:
public ActionResult Show(int id)
{
using (var db = new MyDbContext())
{
var thing = db.Things.Find(id);
return View(thing);
}
}
However, this was causing the ObjectDisposedException listed above whenever I tried to access anything that hadn't been loaded into memory in the View code (in particular, one-to-many relationships in the main view model).
I found a different pattern in this example:
public class MyController : Controller
{
private MyDbContext _db;
public MyController()
{
_db = new MyDbContext();
}
public ActionResult Show(int id)
{
// Do work such as...
var thing = _db.Things.Find(id);
return View(thing);
}
protected override void Dispose(bool disposing)
{
_db.Dispose();
base.Dispose(disposing);
}
}
This pattern keeps the database connection alive until the View has finished rendering, and neatly disposes of it when the Controller itself is disposed.
Here is the problem when you are using
using(var context=new CustomerContext())
{
return View(context.Customers.ToList());
}
when the block of code executes all references are disposed that you are lazzy loading so that's why it is throwing this error.
so i used
return View(context.Customers.ToList()) directly it will work perfectly fine.

Resources