I am trying to get the Views in my xamarin-forms + mvvmcross project to load correctly with no luck.
Project structure breakdown:
Project: Shared.Core - 100% cross platform code, view models, models,
etc..
Project: Shared.Mobile - Xamarin-forms views
Project: iOS - uses shared views
Project: Android - uses shared views
Project: UWP - uses shared views
Project: WPF - uses WPF native views
I have a working WPF project using mvvmcross and am trying to get the mobile going starting with iOS.
The iOS project is only loading the views when the views and viewmodels are in the same assembly. Otherwise I am getting:
Foundation.MonoTouchException: Objective-C exception thrown. Name:
NSInternalInconsistencyException Reason: Application windows are
expected to have a root view controller at the end of application
launch
The same can be seen from this sample project by taking the PCL Views folder and moving it to the iOS project.
https://github.com/MvvmCross/MvvmCross-Forms/tree/master/Samples/Example001XAML
I have also tried the following to no avail:
Setup.cs
protected override IEnumerable<Assembly> GetViewModelAssemblies()
{
var result = base.GetViewModelAssemblies();
var assemblyList = result.ToList();
assemblyList.Add(typeof(FirstViewModel).Assembly);
return assemblyList.ToArray();
}
protected override IEnumerable<Assembly> GetViewAssemblies()
{
var result = base.GetViewAssemblies();
var assemblyList = result.ToList();
assemblyList.Add(typeof(FirstPage).Assembly);
return assemblyList.ToArray();
}
protected override void InitializeViewLookup()
{
base.InitializeViewLookup();
var vmLookup = new Dictionary<Type, Type> {
{typeof (FirstViewModel), typeof (FirstPage)},
{typeof (AboutViewModel), typeof (AboutPage)}
};
var container = Mvx.Resolve<IMvxViewsContainer>();
container.AddAll(vmLookup);
}
I have just fixed this in the Forms presenter core so it now works! You were on the right track with overriding GetViewsAssemblies or InitializeViewLookup. That is how it should work if the presenter had been implemented correctly to begin with.
Anyways, with the new changes in this Pull Request the way it works is:
Either override GetViewsAssemblies to let InitializeViewLookup internally map Views to ViewModels, from the found views in where you tell MvvmCross to look for them. The code in Setup.cs will look something like:
protected override IEnumerable<Assembly> GetViewAssemblies()
{
var result = base.GetViewAssemblies();
var assemblyList = result.ToList();
assemblyList.Add(typeof(FirstPage).Assembly);
return assemblyList;
}
Where FirstPage is one of the pages in an Assembly containing views.
Or you can explicitly tell MvvmCross how to map Views to ViewModels in InitializeViewLookup:
protected override void InitializeViewLookup()
{
base.InitializeViewLookup();
var vmLookup = new Dictionary<Type, Type> {
{typeof (FirstViewModel), typeof (FirstPage)}
};
var container = Mvx.Resolve<IMvxViewsContainer>();
container.AddAll(vmLookup);
}
Related
My environment: ASP.NET Core 5 with RazorPages, Webpack 5.
In razor pages (.cshtml) that reference svg files, I want to inline them. This is something Webpack can do (via a plugin), but I'm not sure how to integrate these two tech stacks.
I could write templatised cshtml files, and populate them via webpack:
ContactUs.cshtml.cs
ContactUs.cshtml <------ read by webpack
ContactUs.generated.cshtml <------ generated by webpack
But then how do I force msbuild / aspnet to use the generated file (ContactUs.generated.cshtml) instead of the template file (ContactUs.cshtml) when building?
I suspect the answer is to use IPageRouteModelConvention but I'm unsure how.
(A dirty workaround is to instead use the filenames ContactUs.template.cshtml and ContactUs.cshtml but I prefer something like the above, as "generated" is clearer.)
UPDATE
To simplify the problem:
The compiler looks for Foo.cshtml.cs and Foo.cshtml.
How do I tell it to instead look for Foo.cshtml.cs and Foo.generated.cshtml?
When loading the app, the framework loads for you a set of PageRouteModels which is auto-generated from the razor page folders (by convention). Each such model contains a set of SelectorModel each one of which has an AttributeRouteModel. What you need to do is just modify that AttributeRouteModel.Template by removing the suffixed part from the auto-generated value.
You can create a custom IPageRouteModelConvention to target each PageRouteModel. However that way you cannot ensure the routes from being duplicated (because after modifying the AttributeRouteModel.Template, it may become duplicate with some other existing route). Unless you have to manage a shared set of route templates. Instead you can create a custom IPageRouteModelProvider. It provides all the PageRouteModels in one place so that you can modify & add or remove any. This way it's so convenient that you can support 2 razor pages in which one page is more prioritized over the other (e.g: you have Index.cshtml and Index.generated.cshtml and you want it to pick Index.generated.cshtml. If that generated view is not existed, the default Index.cshtml will be used).
So here is the detailed code:
public class SuffixedNamePageRouteModelProvider : IPageRouteModelProvider
{
public SuffixedNamePageRouteModelProvider(string pageNameSuffix, int order = 0)
{
_pageNameSuffixPattern = string.IsNullOrEmpty(pageNameSuffix) ? "" : $"\\.{Regex.Escape(pageNameSuffix)}$";
Order = order;
}
readonly string _pageNameSuffixPattern;
public int Order { get; }
public void OnProvidersExecuted(PageRouteModelProviderContext context)
{
}
public void OnProvidersExecuting(PageRouteModelProviderContext context)
{
if(_pageNameSuffixPattern == "") return;
var suffixedRoutes = context.RouteModels.Where(e => Regex.IsMatch(e.ViewEnginePath, _pageNameSuffixPattern)).ToList();
var overriddenRoutes = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var route in suffixedRoutes)
{
//NOTE: this is not required to help it pick the right page we want.
//But it's necessary for other related code to work properly (e.g: link generation, ...)
//we need to update the "page" route data as well
route.RouteValues["page"] = Regex.Replace(route.RouteValues["page"], _pageNameSuffixPattern, "");
var overriddenRoute = Regex.Replace(route.ViewEnginePath, _pageNameSuffixPattern, "");
var isIndexRoute = overriddenRoute.EndsWith("/index", StringComparison.OrdinalIgnoreCase);
foreach (var selector in route.Selectors.Where(e => e.AttributeRouteModel?.Template != null))
{
var template = Regex.Replace(selector.AttributeRouteModel.Template, _pageNameSuffixPattern, "");
if (template != selector.AttributeRouteModel.Template)
{
selector.AttributeRouteModel.Template = template;
overriddenRoutes.Add($"/{template.TrimStart('/')}");
selector.AttributeRouteModel.SuppressLinkGeneration = isIndexRoute;
}
}
//Add another selector for routing to the same page from another path.
//Here we add the root path to select the index page
if (isIndexRoute)
{
var defaultTemplate = Regex.Replace(overriddenRoute, "/index$", "", RegexOptions.IgnoreCase);
route.Selectors.Add(new SelectorModel()
{
AttributeRouteModel = new AttributeRouteModel() { Template = defaultTemplate }
});
}
}
//remove the overridden routes to avoid exception of duplicate routes
foreach (var route in context.RouteModels.Where(e => overriddenRoutes.Contains(e.ViewEnginePath)).ToList())
{
context.RouteModels.Remove(route);
}
}
}
Register the IPageRouteModelProvider in Startup.ConfigureServices:
services.AddSingleton<IPageRouteModelProvider>(new SuffixedNamePageRouteModelProvider("generated"));
I am using ABP v4.9.0 (.NET CORE 2.2) with angular client
I built some custom localization providers. These providers get translation dictionaries from an external API.
I add localization sources on startup with these providers.
var customProvider = new CustomLocalizationProvider(...);
var localizationSource = new DictionaryBasedLocalizationSource("SOURCENAME", customProvider );
config.Localization.Sources.Add(localizationSource );
On startup, the providers InitializeDictionaries() is called and localization dictionaries are built.
So far, so good, working as intended.
Now i'd like to manually Reload these translations on demand, but I can't make this working.
Here is what I tried.
Here I trigger the re-synchronize of the language ressources:
foreach (var localizationSource in _localizationConfiguration.Sources)
{
try
{
localizationSource.Initialize(_localizationConfiguration, _iocResolver);
}
catch (Exception e)
{
Logger.Warn($"Could not get Localization Data for source '{localizationSource.Name}'", e);
}
}
In the custom provider, I first clear the Dictionaries
public class CustomLocalizationProvider : LocalizationDictionaryProviderBase
{
protected int IterationNo = 0;
protected override void InitializeDictionaries()
{
Dictionaries.Clear();
IterationNo += 1;
var deDict = new LocalizationDictionary(new CultureInfo("de-DE"));
deDict["HelloWorld"] = $"Hallo Welt Nummer {IterationNo}";
Dictionaries.Add("de-DE", deDict);
var enDict = new LocalizationDictionary(new CultureInfo("en"));
enDict["HelloWorld"] = $"Hello World number {IterationNo}";
Dictionaries.Add("en", enDict);
}
}
The provider is executed again as expected.
But when I eventually use the localization clientside (angular), I still get the original translations.
What am I missing?
Thanks for the help.
In the meanwhile I had to go for another approach.
I am now using a XmlEmbeddedFileLocalizationDictionaryProvider wrapped by a MultiTenantLocalizationDictionaryProvider.
This way, I am using db-localizations with xml-sources as fallback
Then I manually load the ressources from my API in some appservice. These localizations are then updated in the database by using LanguageTextManager.UpdateStringAsync().
Upgrading to asp.net core 2.2 in my hobby project there is a new routing system I want to migrate to. Previously I implemented a custom IRouter to be able to set the controller for the request dynamically. The incoming request path can be anything. I match the request against a database table containing slugs and it looks up the a matching data container class type for the resolved slug. After that I resolve a controller type that can handle the request and set the RouteData values to the current HttpContext and passing it along to the default implementation for IRouter and everything works ok.
Custom implementaion of IRouter:
public async Task RouteAsync(RouteContext context)
{
var requestPath = context.HttpContext.Request.Path.Value;
var page = _pIndex.GetPage(requestPath);
if (page != null)
{
var controllerType = _controllerResolver.GetController(page.PageType);
if (controllerType != null)
{
var oldRouteData = context.RouteData;
var newRouteData = new RouteData(oldRouteData);
newRouteData.Values["pageType"] = page.PageType;
newRouteData.Values["controller"] = controllerType.Name.Replace("Controller", "");
newRouteData.Values["action"] = "Index";
context.RouteData = newRouteData;
await _defaultRouter.RouteAsync(context);
}
}
}
A controller to handle a specific page type.
public class SomePageController : PageController<PageData>
{
public ActionResult Index(PageData currentPage)
{
return View("Index", currentPage);
}
}
However I got stuck when I'm trying to figure out how I can solve it using the new system. I'm not sure where I'm suppose to extend it for this behavior. I don't want to turn off the endpoint routing feature because I see an opportunity to learn something. I would aso appreciate a code sample if possible.
In ASP.NET 3.0 there is an new dynamic controller routing system. You can implement DynamicRouteValueTransformer.
Documentation is on the way, look at the github issue
I am trying to use a custom (derived) RazorViewEngine AND precompiled views using RazorGenerator.
Some context:
We have a base product that we use for multiple client implementations. With that we have a core set of base views. Most of the views work most of the time. Right now we end up copying existing views for each new solution and modifying as needed. This ends up with 95% of the views being the same between clients and 5% changed.
What I want to do take a base set of views, compile them into a DLL and re-use it across clients. So far I have that working well using RazorGenerator.
Now the next step is to allow for customization (overrides) of views. There is a caveat though. Our application has two "modes" that a user is in. The mode they are in could require a different view.
I have created a derived class from the RazorGeneratorView. This view basically inspects the "OrderingMode" from a UserProfile object that Autofac resolves. Based on the mode - the Path Locator is replaced for the view resolution.
The idea being individual client applications will attempt to resolve the view first in the traditional Views folder. Only I am adding in a sub-directory of Views/{OrderingMode}/{Controller}/{View}.cshtml.
If the view is not found - then it will look in the compiled library (the core views).
This allows me to override individual views / partials as need be for clients.
public PosViewEngine() : base()
{
//{0} = View Name
//{1} = ControllerName
//{2} = Area Name
AreaViewLocationFormats = new[]
{
//First look in the hosting application area folder / Views / ordering type
//Areas/{AreaName}/{OrderType}/{ControllerName}/{ViewName}.cshtml
"Areas/{2}/Views/%1/{1}/{0}.cshtml",
//Next look in the hosting application area folder / Views / ordering type / Shared
//Areas/{AreaName}/{OrderType}/{ControllerName}/{ViewName}.cshtml
"Areas/{2}/Views/%1/Shared/(0}.cshtml",
//Finally look in the IMS.POS.Web.Views.Core assembly
"Areas/{2}/Views/{1}/{0}.cshtml"
};
//Same format logic
AreaMasterLocationFormats = AreaViewLocationFormats;
AreaPartialViewLocationFormats = new[]
{
//First look in the hosting application area folder / Views / ordering type
//Areas/{AreaName}/{OrderType}/{ControllerName}/Partials/{PartialViewName}.cshtml
"Areas/{2}/Views/%1/{1}/Paritals/{0}.cshtml",
//Next look in the hosting application area folder / Views / ordering type / Shared
//Areas/{AreaName}/{OrderType}/{ControllerName}/{ViewName}.cshtml
"Areas/{2}/Views/%1/Shared/(0}.cshtml",
//Finally look in the IMS.POS.Web.Views.Core
"Areas/{2}/Views/{1}/{0}.cshtml"
};
ViewLocationFormats = new[]
{
"Views/%1/{1}/{0}.cshtml",
"Views/%1/Shared/{0}.cshtml",
"Views/{1}/{0}.cshtml",
"Views/Shared/{0}.cshtml"
};
MasterLocationFormats = ViewLocationFormats;
PartialViewLocationFormats = new[]
{
"Views/%1/{1}/Partials/{0}.cshtml",
"Views/%1/Shared/{0}.cshtml",
"Views/{1}/Partials/{0}.cshtml",
"Views/Shared/{0}.cshtml"
};
}
protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath)
{
return base.CreatePartialView(controllerContext, partialPath.ReplaceOrderType(CurrentOrderingMode()));
}
protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath)
{
OrderType orderType = CurrentOrderingMode();
return base.CreateView(controllerContext, viewPath.ReplaceOrderType(orderType), masterPath.ReplaceOrderType(orderType));
}
protected override bool FileExists(ControllerContext controllerContext, string virtualPath)
{
return base.FileExists(controllerContext, virtualPath.Replace("%1/",string.Empty));
}
private OrderType CurrentOrderingMode()
{
OrderType result;
_profileService = DependencyResolver.Current.GetService<IUserProfileService>();
if (_profileService == null || _profileService.OrderingType == 0)
{
IApplicationSettingService settingService =
DependencyResolver.Current.GetService<IApplicationSettingService>();
result =
settingService.GetApplicationSetting(ApplicationSettings.DefaultOrderingMode)
.ToEnumTypeOf<OrderType>();
}
else
{
result = _profileService.OrderingType;
}
return result;
}
}
Here is the StartUp class RazorGenerator uses to Register the ViewEngine.
public static class RazorGeneratorMvcStart
{
public static void Start()
{
var engine = new PrecompiledMvcEngine(typeof(RazorGeneratorMvcStart).Assembly)
{
UsePhysicalViewsIfNewer = HttpContext.Current.Request.IsLocal
};
ViewEngines.Engines.Insert(0, engine);
// StartPage lookups are done by WebPages.
VirtualPathFactoryManager.RegisterVirtualPathFactory(engine);
}
}
The problem is:
This code is executed last (after I register the PosViewEngine) and it inserts the engine at the first position (meaning this is the engine that gets resolved 1st when serving up responses). This ends up finding a view - it is the core view.
If I change the code in the StartUp to Register my custom view engine first first and then the RazorGenerator engine
public static void Start()
{
var engine = new PrecompiledMvcEngine(typeof(RazorGeneratorMvcStart).Assembly)
{
UsePhysicalViewsIfNewer = HttpContext.Current.Request.IsLocal
};
ViewEngines.Engines.Clear();
ViewEngines.Engines.Insert(0, new PosViewEngine());
ViewEngines.Engines.Insert(1, engine);
// StartPage lookups are done by WebPages.
VirtualPathFactoryManager.RegisterVirtualPathFactory(engine);
}
I end up with an exception on the FileExists(ControllerContext controllerContext, string virtualPath) method - "The relative virtual path 'Views/Account/LogOn.cshtml' is not allowed here."
It obviously has something to do with both physical and virtual paths being mixed together.
It looks like someone else was trying to do the same thing here but I didn't see an answer on this.
For anyone else wanting to try this approach I'll post the answer. Basically you need to implement a custom view engine that derives from the PrecompiledMvcEngine found in the RazorGenerator assembly.
public class PosPrecompileEngine : PrecompiledMvcEngine
{
private IUserProfileService _profileService;
public PosPrecompileEngine(Assembly assembly) : base(assembly)
{
LocatorConfig();
}
public PosPrecompileEngine(Assembly assembly, string baseVirtualPath) : base(assembly, baseVirtualPath)
{
LocatorConfig();
}
public PosPrecompileEngine(Assembly assembly, string baseVirtualPath, IViewPageActivator viewPageActivator) : base(assembly, baseVirtualPath, viewPageActivator)
{
LocatorConfig();
}
protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath)
{
return base.CreatePartialView(controllerContext, partialPath.ReplaceOrderType(CurrentOrderingMode()));
}
protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath)
{
OrderType orderType = CurrentOrderingMode();
return base.CreateView(controllerContext, viewPath.ReplaceOrderType(orderType), masterPath.ReplaceOrderType(orderType));
}
protected override bool FileExists(ControllerContext controllerContext, string virtualPath)
{
return base.FileExists(controllerContext, virtualPath.ReplaceOrderType(CurrentOrderingMode()));
}
}
In this class - I override the Locator Paths. Because I have the "base" compiled views in another assembly from the web application - we implemented a convention where the view engine will first look in a PosViews/{ordering mode}/{controller}/{view} path in the web application. If a view is not located -then it will look in the traditional /Views/controller/view. The trick here is the later is a virtual path located in another class library.
This allowed us to "override" an existing view for the application.
private void LocatorConfig()
{
//{0} = View Name
//{1} = ControllerName
//{2} = Area Name
AreaViewLocationFormats = new[]
{
//First look in the hosting application area folder / Views / ordering type
//Areas/{AreaName}/{OrderType}/{ControllerName}/{ViewName}.cshtml
"PosAreas/{2}/Views/%1/{1}/{0}.cshtml",
//Next look in the hosting application area folder / Views / ordering type / Shared
//Areas/{AreaName}/{OrderType}/{ControllerName}/{ViewName}.cshtml
"PosAreas/{2}/Views/%1/Shared/(0}.cshtml",
//Next look in the POS Areas Shared
"PosAreas/{2}/Views/Shared/(0}.cshtml",
//Finally look in the IMS.POS.Web.Views.Core assembly
"Areas/{2}/Views/{1}/{0}.cshtml"
};
//Same format logic
AreaMasterLocationFormats = AreaViewLocationFormats;
AreaPartialViewLocationFormats = new[]
{
//First look in the hosting application area folder / Views / ordering type
//Areas/{AreaName}/{OrderType}/{ControllerName}/Partials/{PartialViewName}.cshtml
"PosAreas/{2}/Views/%1/{1}/Partials/{0}.cshtml",
//Next look in the hosting application area folder / Views / ordering type / Shared
//Areas/{AreaName}/{OrderType}/{ControllerName}/{ViewName}.cshtml
"PosAreas/{2}/Views/%1/Shared/(0}.cshtml",
//Next look in the hosting application shared folder
"PosAreas/{2}/Views/Shared/(0}.cshtml",
//Finally look in the IMS.POS.Web.Views.Core
"Areas/{2}/Views/{1}/{0}.cshtml"
};
ViewLocationFormats = new[]
{
"~/PosViews/%1/{1}/{0}.cshtml",
"~/PosViews/%1/Shared/{0}.cshtml",
"~/PosViews/Shared/{0}.cshtml",
"~/Views/{1}/{0}.cshtml",
"~/Views/Shared/{0}.cshtml"
};
MasterLocationFormats = ViewLocationFormats;
PartialViewLocationFormats = new[]
{
"~/PosViews/%1/{1}/{0}.cshtml",
"~/PosViews/%1/Shared/{0}.cshtml",
"~/PosViews/Shared/{0}.cshtml",
"~/Views/{1}/{0}.cshtml",
"~/Views/Shared/{0}.cshtml"
};
}
Register this engine in your application start up events.
public static void Configure()
{
var engine = new PosPrecompileEngine(typeof(ViewEngineConfig).Assembly)
{
UsePhysicalViewsIfNewer = true,
PreemptPhysicalFiles = true
};
ViewEngines.Engines.Add(engine);
// StartPage lookups are done by WebPages.
VirtualPathFactoryManager.RegisterVirtualPathFactory(engine);
}
Here is the final key. When RazorGenerator gets installed view NuGet - you end up with this start-up class that will run on startup
[assembly: WebActivatorEx.PostApplicationStartMethod(typeof(Views.Core.RazorGeneratorMvcStart), "Start")]
public static class RazorGeneratorMvcStart
{
public static void Start()
{
var engine = new PrecompiledMvcEngine(typeof(RazorGeneratorMvcStart).Assembly)
{
UsePhysicalViewsIfNewer = true,
PreemptPhysicalFiles = true
};
ViewEngines.Engines.Add(engine);
// StartPage lookups are done by WebPages.
VirtualPathFactoryManager.RegisterVirtualPathFactory(engine);
}
}
By default - RazorGenerator adds ViewEngine to the first in the collection
ViewEngines.Engines.Insert(0,engine);
You need to change that to an add
ViewEngines.Engines.Add(engine);
So it is added to engines last - this way your custom ViewEngine is used FIRST in locating views.
This approach allows you to reuse views in multiple applications while allowing a means to override that view.
This may be overkill for most applications - bust as I mentioned in the question - this is base product that we use to develop multiple client applications. Trying achieve reuse while maintaining a level of flexibility on a per client basis is something we were trying to achieve.
I'd like to know what is the best solution to manipulate application settings in a cross-platform way.
In iOS we can change the settings outside the app in the settings screen, but we don't have that in windows phone and android.
So, my idea is to create a normal page/screen inside the app that shows all my application settings and have an interface with Save() and Get() methods that I can implement specific per device using DependencyServices.
Is this the right way to do it?
The Application subclass has a static Properties dictionary which can be used to store data. This can be accessed from anywhere in your Xamarin.Forms code using Application.Current.Properties.
Application.Current.Properties ["id"] = someClass.ID;
if (Application.Current.Properties.ContainsKey("id"))
{
var id = Application.Current.Properties ["id"] as int;
// do something with id
}
The Properties dictionary is saved to the device automatically. Data added to the dictionary will be available when the application returns from the background or even after it is restarted. Xamarin.Forms 1.4 introduced an additional method on the Application class - SavePropertiesAsync() - which can be called to proactively persist the Properties dictionary. This is to allow you to save properties after important updates rather than risk them not getting serialized out due to a crash or being killed by the OS.
https://developer.xamarin.com/guides/cross-platform/xamarin-forms/working-with/app-lifecycle/
Xamarin.Forms plugin which uses the native settings management.
Android: SharedPreferences
iOS: NSUserDefaults
Windows Phone: IsolatedStorageSettings
Windows Store / Windows Phone RT: ApplicationDataContainer
https://github.com/jamesmontemagno/Xamarin.Plugins/tree/master/Settings
I tried using the Application.Current.Properties Dictionary and had implementation problems.
A solution that worked with very little effort was James Montemagno's Xam.Plugin.Settings NuGet. GitHub Installing the NuGet automagically creates a Helpers folder with Settings.cs. To create a persisted setting you do:
private const string QuestionTableSizeKey = "QuestionTableSizeKey";
private static readonly long QuestionTableSizeDefault = 0;
and
public static long QuestionTableSize
{
get
{
return AppSettings.GetValueOrDefault<long>(QuestionTableSizeKey, QuestionTableSizeDefault);
}
set
{
AppSettings.AddOrUpdateValue<long>(QuestionTableSizeKey, value);
}
}
Access and setting in the app then looks like:
namespace XXX
{
class XXX
{
public XXX()
{
long myLong = 495;
...
Helpers.Settings.QuestionTableSize = myLong;
...
long oldsz = Helpers.Settings.QuestionTableSize;
}
}
}