Dynamically include precompiled (Razor) views in .NET Core 2.1 - asp.net-core

Idea
I'm trying to build a modular .NET Core MVC application, were I can add 'modules' (extensions) just by copying the required libraries to a predefined folder. The application defines a set of dependencies (DI), error handling and basic layout; the modules provide the actual application or page logic.
We've build a proof of concept, using ExtCore - the concept works great, Controllers are added automatically and the 'plain' Views are recognized easily (the ones set as 'Embedded Resource').
Challenge
However, I don't like the fact that Views are set to 'Embedded Resource', as it will be a small performance hit as the Razor page is now build at runtime.
What we've tried
As .NET Core 2.1 now has the ability to create Reusable Razor UI, I've created a project with a reference to <Project Sdk="Microsoft.NET.Sdk.Razor"> and compiled it to a Razor library (*.Views.dll).
When I link the Razor View library as project dependency in (main) application, everything works fine. However, the goal still is to make the application fully plug-and-play and allow libraries to be included just by deploying them to a folder.
I've tried to achieve this extending the Razor View Engine using a PhysicalFileProvider, but it won't recognize the views in the library.
services.Configure<RazorViewEngineOptions>(opt =>
{
opt.FileProviders.Add(new PhysicalFileProvider("path/to/extension"));
});
The result always is an exception telling me that /Views/[controller]/[action].cshtml is not found.
Searching other answers I came across this project, which creates a ViewLocationExpander. I tried implementing various versions of this idea without luck - most likely because this is an older concept based on .NET Core 1.x which still uses a project dependency; although the Razor View engine now searches the namespace of the assembly.
services.Configure<RazorViewEngineOptions>(opt =>
{
opt.FileProviders.Add(new PhysicalFileProvider("path/to/extension");
opt.ViewLocationExpanders.Add(new DynamicLocationExpander());
});
Where DynamicLocationExpander looks like:
public class DynamicLocationExpander: IViewLocationExpander
{
public DynamicLocationExpander()
{
}
public void PopulateValues(ViewLocationExpanderContext context)
{
}
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
{
string assembly = ((ControllerActionDescriptor)context.ActionContext.ActionDescriptor)
.ControllerTypeInfo.AsType().Assembly.GetName().Name;
foreach (var viewLocation in viewLocations)
yield return $"{assembly}/{viewLocation}";
}
}
Question
How can we dynamically link a Razor View library, containing precompiled views, to the Razor Engine in a (parent) application?

Related

ASP.NET Core 6 MVC - access a view (.cshtml) page from a different class library project

We have a scenario where we have to move couple of our view pages away from our Web API project to a separate class library. Where this class library will be be consumed by different Web API projects that needs to load these shared View pages as part of the functionality. I have been looking for a day now but cannot find a way how to do it.
The view pages work with no problems when accessed from within the Web API project but we have now moved these View pages into our existing common library (a class library) and added it as a reference to the Web API project. Basically when we build the application with the common class library containing the views (we changed the property to Content so it gets added on build time), it gets built and copied into the bin folder. So from this, we could say that the view files should be reachable as its just within the project assembly bin folder.
What happens now is that even if setting the web application builder to specify the Content directory to point to this, it still cannot see the View pages and I get an error
The view was not found
Code:
var builder = WebApplication.CreateBuilder(new WebApplicationOptions {
Args = args,
ContentRootPath = PlatformServices.Default.Application.ApplicationBasePath
});
What bugs me is that the same /Views folder is generated when we put back the View pages back to the Web API project. Same structure and files. Only when put to another project, it now cannot recognize it. Having the view pages on the Web API works while putting it to another project does not.
This is a required structure that we need to implement without the use of a RCL but would still work when referenced by different Web APIs. This may seem odd but this is what we need to do and if possible with only minimal changes.
Your help is very much appreciated!
After a few tries, we we're able to do this by setting the resource object to Embedded Resource and implementing the ManifestEmbeddedFileProvider in the common library to virtually map the location when it gets published as a NuGet. In this case, say like we have a folder named /StaticResources in our common lib. In the sample code below, Program refers to your program assembly or any class object within your application.
Code:
// Get embedded file assembly path to allow our static files to be read by the consuming apps
var manifestEmbeddedProvider = new ManifestEmbeddedFileProvider(
typeof(Program).Assembly,
"/StaticResources");
// Sets the `/StaticResources` folder to be servable like a wwwroot folder
app.UseStaticFiles(new StaticFileOptions {
FileProvider = manifestEmbeddedProvider,
RequestPath = "/Resources"
});
// Use it like this
<script src="/Resources/MyScrtipt.js"></script>
For the View() to work, assuming your views are in /StaticResources folder.
var viewsFileProvider = new ManifestEmbeddedFileProvider(
typeof(Program).Assembly,
"/StaticResources");
app.UseStaticFiles(new StaticFileOptions {
FileProvider = viewsFileProvider,
RequestPath = "/Views/Shared"
});
Hope this helps anyone who comes across this issue. Thank you for all of you who have shared their answers.

Mobile Specific Views / Device Detection

In the .NET Core docs there is a page titled "Building Mobile Specific Views" but is under construction: https://docs.asp.net/en/latest/mvc/views/mobile.html.
Does anyone have some insight on building mobile views or successfully doing device detection?
Serving specific views based on the browser's user-agent is an outdated concept as it do not sufficiently says much about the capabilities of the device. For example, iPhone and iPad come in different screen sizes and even mobile browsers allow to change the user-agent.
The new concept is called Responsive Design where one creates a single page that fits and show/hides certain element based on the available screen width. One popular responsive deisgn CSS Framework is Bootstrap, originally developed by Twitter and later open-sourced.
Here is an example of responsive design. When you go to the site and change the width of your browser, the design updates as well from 3 to 2 to 1 column design with browser or mobile like navigation (with the Hamburger menu).
This feature actually was not implemented by microsoft. There is couple open discussions for this question:
https://github.com/aspnet/Mvc/issues/4877
https://github.com/aspnet/Razor/issues/751
As a generic answer from them - use responsive web design and css media queries (which from my point of view is not perfect answer for team that claims himself for building general web framework).
There is a implementation for this feature exist as pull request - https://github.com/aspnet/Mvc/pull/4878.
Since this pull request seems to be forgotten, i extract this code into separate project which is available on
https://github.com/laskoviymishka/MvcDeviceDetector.
You may use this implementation (which is easy to add to exist MVC project) or implement this itself. This is pretty easy - you need just implement and reqister own IViewLocationExpander for that.
This can be handle in .Net Core using RazorViewEngineOptions
1) Create Service LocationExpanderService.cs
public class LocationExpanderService : IViewLocationExpander
{
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context,
IEnumerable<string> viewLocations)
{
//replace the Views to MyViews..
var viewLocationsFinal = new List<string>();
if (!string.IsNullOrEmpty(context.Values["viewCustom"]))
{
foreach (var viewLocation in viewLocations)
{
viewLocationsFinal.Add(viewLocation.Replace(".cshtml", ".mobile.cshtml"));
}
}
viewLocationsFinal.AddRange(viewLocations);
return viewLocationsFinal;
}
public void PopulateValues(ViewLocationExpanderContext context)
{
var userAgent = context.ActionContext.HttpContext.Request.Headers["User-Agent"].ToString().ToLower();
var viewCustom = userAgent.Contains("android") || userAgent.Contains("iphone") ? "mobile" : "";
context.Values["viewCustom"] = viewCustom;
}
}
2) Configure services in startup.cs
services.Configure<RazorViewEngineOptions>(o =>
{
o.ViewLocationExpanders.Add(new LocationExpanderService());
});
3) Create view with .mobile extension
Index.mobile.cshtml

Remove version control from Eclipose RCP project

I have an Eclipse RCP application that uses EGit and JGit to implement the version control of projects.
In some cases, I want to make it so that a project is no longer under version control. This implies both deleting the project's .git directory on the file system, and making my application aware that the project is not under version control.
I tried looking through the JGit API for solutions to this, but could not find anything useful. Simply deleting the .git directory using a file manipulation API is not sufficient, as some files in it seem to be locked (very likely by JGit).
Sorry, I'm not 100% sure because your scenario is not clear enough to me, however you said that your application is based also on EGit, so I assume that you are using the standard eclipse APIs to manage the projects. If this is the case, what you are looking for is: org.eclipse.team.core.RepositoryProvider
This is an example:
public class UntrackProject extends AbstractHandler implements IHandler {
#Override
public Object execute(ExecutionEvent event) throws ExecutionException {
IProject project = getProject(); // get the project, for example using the selection service
try {
RepositoryProvider.unmap(project);
//TODO Refresh your viewer to show changes
} catch (TeamException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
}
For reference, you can check the handler of the context menu "Disconnect", that does exactly what you are looking for: org.eclipse.egit.ui.internal.actions.DisconnectActionHandler
https://git.eclipse.org/c/egit/egit.git/tree/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/actions/DisconnectActionHandler.java
It uses a lot of internal classes of egit, but in the end it uses the RepositoryProvider's API.
Note that the method RepositoryProvider.unmap(project) does NOT delete the repository, it just disconnects the project.
I hope this helps!

ASP.NET MVC 4 ScriptBundle returns empty

Specifically, I am trying to create a ScriptBundle in MVC 4 with already minified scripts, and return this same Bundle whether the project is in Debug or not.
My web project references the MVC Telerik Grid NuGet package. In that package, Telerik only provides the minified JS files. Bundling code is below.
// telerik scripts
bundles.Add(new ScriptBundle("~/scripts/bundles/telerik").Include(
"~/Scripts/2012.1.214/telerik.common.min.js",
"~/Scripts/2012.1.214/telerik.textbox.min.js",
"~/Scripts/2012.1.214/telerik.calendar.min.js",
"~/Scripts/2012.1.214/telerik.datepicker.min.js",
"~/Scripts/2012.1.214/telerik.grid.min.js",
"~/Scripts/2012.1.214/telerik.grid.filtering.min.js"));
Other ScriptBundles run fine, but when my project attempts to reference this bundle, the request appears as: scripts/bundles/telerik?v= Returning nothing.
If I set BundleTable.EnableOptimizations = true, then it DOES return the ScriptBundle and references a specific version, however this solution is unacceptable.
I do not want to forcibly set BundleTable.EnableOptimizations = true, since I want all other Bundles to return the non-minified versions when appropriate.
Anyone have a similar experience and if so, what was the solution?
I think you have the same problem, please look at this link: mvc4 bundler not including .min files
Either rename .min.js to .js or do something like:
public static void AddDefaultIgnorePatterns(IgnoreList ignoreList)
{
if (ignoreList == null)
throw new ArgumentNullException("ignoreList");
ignoreList.Clear();
ignoreList.Ignore("*.intellisense.js");
ignoreList.Ignore("*-vsdoc.js");
ignoreList.Ignore("*.debug.js", OptimizationMode.WhenEnabled);
//ignoreList.Ignore("*.min.js", OptimizationMode.WhenDisabled);
ignoreList.Ignore("*.min.css", OptimizationMode.WhenDisabled);
}

MEF + SL4 question

I'm working on an app in the Silverlight 4 RC and i'm taking the oppertunity to learn MEF for handling plugin controls. I've got it working in a pretty basic manor, but it's not exactly tidy and I know there is a better way of importing multiple xap's.
Essentially, in the App.xaml of my host app, I've got the following telling MEF to load my xap's:
AggregateCatalog catalog = new AggregateCatalog();
DeploymentCatalog c1 = new DeploymentCatalog(new Uri("TestPlugInA.xap", UriKind.Relative));
DeploymentCatalog c2 = new DeploymentCatalog(new Uri("TestPlugInB.xap", UriKind.Relative));
catalog.Catalogs.Add(c1);
catalog.Catalogs.Add(c2);
CompositionHost.Initialize(catalog);
c1.DownloadAsync();
c2.DownloadAsync();
I'm sure I'm not using the AggregateCatalog fully here and I need to be able to load any xap's that might be in the directory, not just hardcoding Uri's obviously....
Also, in the MainPage.xaml.cs in the host I have the following collection which MEF puts the plugin's into:
[ImportMany(AllowRecomposition = true)]
public ObservableCollection<IPlugInApp> PlugIns { get; set; }
Again, this works, but I'm pretty sure I'm using ImportMany incorrectly....
Finally, the MainPage.xaml.cs file implements IPartImportsSatisfiedNotification and I have the following for handling the plugin's once loaded:
public void OnImportsSatisfied()
{
sp.Children.Clear();
foreach (IPlugInApp plugIn in PlugIns)
{
if (plugIn != null)
sp.Children.Add(plugIn.GetUserControl());
}
}
This works, but it seems filthy that it runs n times (n being the number of xap's to load). I'm having to call sp.Children.Clear() as if I don't, when loading the 2 plugin's, my stack panel is populated as follows:
TestPlugIn A
TestPlugIn A
TestPlugIn B
I'm clearly missing something here. Can anyone point out what I should be doing?
Thanks!
I think most of what you are doing is fine. Although ObservableCollections do support notifications of individual elements being added and removed, MEF doesn't take advantage of this. In your case it will simply clear the collection and then add all the plugins. Since you are using OnImportsSatisfied for the change notification, you don't even need an ObservableCollection. You could just use an IEnumerable for your import.
To add flexibility in downloading different xaps, I would expose a service in your container that can be imported and that provides the functionality to download a xap given a url. Then any component in your container can trigger a download, and the url to download can come from whatever source you deem appropriate.