I need to create a nuget package which will contain shared views, controllers, js, and css files to be used across multiple other projects. Essentially a modular set of things like checkout or search pages that can be dropped into other site projects.
All the research I've done so far points to utilizing precompiled views with RazorGenerator but doesn't say much about the controllers, js, and css files.
Ideally a module's views and other files should be able to be overridden by the consuming host project but the files themselves should not be directly editable within the host project. Much like dlls referenced when other nuget packages are added.
The answers and posts about this type of subject I've found so far seem a bit dated.
Is there a cleaner, modern solution for creating an ASP.NET MVC module nuget package so that fully working pages are able to be shared across projects?
Controllers
Use area's and register those area's. Possibly this is not natively supported and you might need to overwrite some parts in mvc4. look at:
How do I register a controller that has been created in an AREA
http://netmvc.blogspot.be/2012/03/aspnet-mvc-4-webapi-support-areas-in.html
As long as the dll's are loaded in you can always register all classes that are subclasses of Controller with reflection (on application startup).
Razor
Precompiling is possible, but only really adviseable in dotnet core since it is a first class citizen there.
You could also add the views as content which is injected into the project.
Downside:
On update it overwrites the views (if you changed them you lost changes)
Pros:
On update you can merge both changes in git
Easily change the already existing razor pages
I found a solution that works for us. We have not yet fully implemented this so some unforeseen issues may still arise.
First, create an MVC project with the needed views, controllers, javascript, etc needed to match your package requirements. Each static file and view must be set as embedded resources in the project.
Then, add a class to serve these files on a virtual path provider. This will allow a consuming project to access the static files and views as if they were within the same project.
To enable custom routing an implementation of the RouteBase class will be required. This implementation needs to accept a string property for which virtual routes are based to allow the host to apply whichever route prefix is desired. For our example, the property will default to Booking with the associated architecture of views within our project to match.
Both the RouteBase implementation and the VirtualPath class will be instantiated within a setup method. This will allow a consuming project to call a single method to setup the booking engine. This method will take in the sites Route Collection and dynamic route property to append the custom routes. The method will also register the VirtualPathProvider to the HostingEnvironment object.
The consuming host can also override views and any other static file by simply having a file in a location within the host project that matches the path of the file or view in the booking engine.
Some Code Examples
RouteBase method which returns correct route values if an incoming route matches a virtual route.
public override RouteData GetRouteData(HttpContextBase httpContext)
{
RouteData result = null;
// Trim the leading slash
var path = httpContext.Request.Path.Substring(1);
// Get the page that matches.
var page = GetPageList(httpContext)
.Where(x => x.VirtualPath.Equals(path))
.FirstOrDefault();
if (page != null)
{
result = new RouteData(this, new MvcRouteHandler());
// Optional - make query string values into route values.
AddQueryStringParametersToRouteData(result, httpContext);
result.Values["controller"] = page.Controller;
result.Values["action"] = page.Action;
}
// IMPORTANT: Always return null if there is no match.
// This tells .NET routing to check the next route that is registered.
return result;
}
RouteBase Virtual Route to NuGet Package Route mapping. New PageInfo objects created with dynamic virtual path string and references to real controller and action names. These are then stored in the http context cache.
private IEnumerable<PageInfo> GetPageList(HttpContextBase httpContext)
{
string key = "__CustomPageList";
var pages = httpContext.Cache[key];
if (pages == null)
{
lock (synclock)
{
pages = httpContext.Cache[key];
if (pages == null)
{
pages = new List<PageInfo>()
{
new PageInfo()
{
VirtualPath = string.Format("{0}/Contact", BookingEngine.Route),
Controller = "Home",
Action = "Contact"
},
};
httpContext.Cache.Insert(
key: key,
value: pages,
dependencies: null,
absoluteExpiration: System.Web.Caching.Cache.NoAbsoluteExpiration,
slidingExpiration: TimeSpan.FromMinutes(1),
priority: System.Web.Caching.CacheItemPriority.NotRemovable,
onRemoveCallback: null);
}
}
}
return (IEnumerable<PageInfo>)pages;
}
Booking Engine class setup method which does all the instantiating needed for the assembly.
public class BookingEngine
{
public static string Route = "Booking";
public static void Setup(RouteCollection routes, string route)
{
Route = route;
HostingEnvironment.RegisterVirtualPathProvider(
new EmbeddedVirtualPathProvider());
routes.Add(
name: "CustomPage",
item: new CustomRouteController());
}
}
EmbeddedVirtualFile
public override CacheDependency GetCacheDependency(string virtualPath, virtualPathDependencies, DateTime utcStart)
{
string embedded = _GetEmbeddedPath(virtualPath);
// not embedded? fall back
if (string.IsNullOrEmpty(embedded))
return base.GetCacheDependency(virtualPath,
virtualPathDependencies, utcStart);
// there is no cache dependency for embedded resources
return null;
}
public override bool FileExists(string virtualPath)
{
string embedded = _GetEmbeddedPath(virtualPath);
// You can override the embed by placing a real file at the virtual path...
return base.FileExists(virtualPath) || !string.IsNullOrEmpty(embedded);
}
public override VirtualFile GetFile(string virtualPath)
{
// You can override the embed by placing a real file at the virtual path...
if (base.FileExists(virtualPath))
return base.GetFile(virtualPath);
string embedded = _GetEmbeddedPath(virtualPath);
if (string.IsNullOrEmpty(embedded))
return null;
return new EmbeddedVirtualFile(virtualPath, GetType().Assembly
.GetManifestResourceStream(embedded));
}
private string _GetEmbeddedPath(string path)
{
if (path.StartsWith("~/"))
path = path.Substring(1);
path = path.Replace(BookingEngine.Route, "/");
//path = path.ToLowerInvariant();
path = System.Reflection.Assembly.GetExecutingAssembly().GetName().Name + path.Replace('/', '.');
// this makes sure the "virtual path" exists as an embedded resource
return GetType().Assembly.GetManifestResourceNames()
.Where(o => o == path).FirstOrDefault();
}
Nested Virtual File Class
public class EmbeddedVirtualFile : VirtualFile
{
private Stream _stream;
public EmbeddedVirtualFile(string virtualPath,
Stream stream) : base(virtualPath)
{
if (null == stream)
throw new ArgumentNullException("stream");
_stream = stream;
}
public override Stream Open()
{
return _stream;
}
}
A lot of the code we are using comes from the following links
Embedded Files - https://www.ianmariano.com/2013/06/11/embedded-razor-views-in-mvc-4/
RouteBase implementation - Multiple levels in MVC custom routing
Related
My Razor page looks like this.
#using Microsoft.AspNetCore.Mvc.Localization
#inject IViewLocalizer Localizer
<h1>#Localizer["Index"]</h1>
...
My Startup.cs contains the following.
public void ConfigureServices(IServiceCollection services)
{
...
services.AddLocalization(a => a.ResourcesPath = "/");
services.Configure<RequestLocalizationOptions>(a =>
{
CultureInfo[] supportedCultures = {
new CultureInfo("sv-SE"),
new CultureInfo("se")
};
a.DefaultRequestCulture = new RequestCulture("se");
a.SupportedCultures = supportedCultures;
a.SupportedUICultures = supportedCultures;
});
...
}
I placed a file called Controllers.HomeController.se.resx directly in the project's root. The controller HomeController contains the injection.
public class HomeController : Controller
{
private readonly Context _context;
private readonly IStringLocalizer<HomeController> _localizer;
public HomeController(Context context, IStringLocalizer<HomeController> localizer)
{
_context = context;
_localizer = localizer;
}
...
}
The application doesn't crash but the string renedered is Index and not the value from the RESX file. I've tried to follow the docs as closely as possible but apparently I've missed something. I need help finding what that would be.
I breakpointed and checked the value of _localizer["Index"] in the constructor. As expected, the flag for the file not being found is set to true. Checking the value of SearchedLocation gives me Web...Controllers.MemberController. I can't tell if those three dots is the correct one for the RESX file in the project's root. I was expecting se somewhere in the name too.
If you want to place you resources files in root of the project you should set ResourcesPath as following
services.AddLocalization(a => a.ResourcesPath = ""); //empty string
With this settings SearchedLocation will give you Web.Controllers.MemberController which points to Controllers.MemberController.resx file in the root of the project.
To use localization in view you have to follow Views.{ControllerName}.{ViewName}.resx pattern. For example if you have HomeController and About view in it you need to have Views.Home.About.resx file to use localization.
Another convention resource reader follows when searching for localization files is searching the files in respective folders rather than by dot separated names. For example if ResourcesPath is set to "Resources" the following variants are equal
Resources.Views.Home.About.resx
Resources\Views.Home.About.resx
Resources\Views\Home.About.resx
Resources\Views\Home\About.resx
So it's possible to structure your localization files by folders.
And you didn't specify you added app.UseRequestLocalization() in your Startup.cs. If you don't do this your application won't be able to determine request culture and it will always point to default resource file. Read more in the docs.
Note
There are 2 ways of configuring RequestLocalizationOptions for request localization, via services.Configure<RequestLocalizationOptions> or passing constructed options object (or delegate) to app.UseRequestLocalization. Effectively there is no difference between these approaches, they are totally equal in terms of localization middleware. But if at any point of an application you need to get RequestLocalizationOptions you won't be able to get value passed to app.UseRequestLocalization. But it is easy to accompish with services.Configure<RequestLocalizationOptions> (it's general approach described in the docs)
public class HomeController : Controller
{
private readonly RequestLocalizationOptions _requestLocalizationOptions;
public HomeController(IOptions<RequestLocalizationOptions> options)
{
_requestLocalizationOptions = options.Value;
}
//..
}
When you create code in Visual Studio, it runs from a temp directory. But at a hosting provider (one that specifically hosts ASP.NET sites), it will be in a different directory (typically an app server virtual folder with IIS). I need to have uploaded files in a directory the IIS web server can find them, so the use case below will work.
Q: How can I define my links in MVC Controllers/Views so that they work both in development as well as in production (upload & download)?
Use Case: My app needs to allow a file to be uploaded (still don't know where on dev box to upload to), and then via a link on another page download that same file.
In case you're in need of a more detailed answer I'll give you some hints and hopefully this will get you on the right track.
Define a contract so you can abstract any concrete file provider implementation in order to minimize the impact and modifications to your code if you need to change the underlying file storage later. For instance if you go to Azure in a future version you can implement an AzureFileProvider and store your file using Blobs.
public interface IFileProvider
{
UploadResult UploadFile(byte[] fileContent, string fileName);
DownloadResult DownloadFile(int fileId);
}
Implement your interface for a file system-based file provider:
public class FileSystemFileProvider : IFileProvider
{
public FileSystemFileProvider(string absouteBasePath)
{
//Check if absoluteBasPath exists and create the directory if it doesn't.
if (!Directory.Exists(absouteBasePath))
Directory.CreateDirectory(absouteBasePath);
}
public UploadResult UploadFile(byte[] fileContent, string fileName)
{
//TODO: Your upload file implementation here
}
public DownloadResult DownloadFile(int fileId)
{
//TODO Your download file implementation here
}
}
You can adjust your contract according to your needs. Here in this sample UploadResult and DownloadResult are simple classes to retrieve the result of the operations. For example this could be a definition for DownloadResult:
public class DownloadResult
{
public bool Success { get; set; }
public byte[] FileContent { get; set; }
public string DownloadName { get; set; }
public string SizeInBytes { get; set; }
//... any other useful properties
}
In your StartupAuth.cs or Global.asax (the place where your bootstrapper code runs, initialize the file provider with your concrete implementation. This approach works better with an IoC container in place. You can use AutoFac, Ninject or any other you like, but please do your self a favor and use one.
//read the base path from web.config so you may adjust it on production hosting provider without need to make and compile the app
var fileBasePath = ConfigurationManager.AppSettings["fileBasePath"];
//get the path as an absolute file system path
var absoluteBasePath = Server.MapPath(fileBasePath);
//Initialize the file provider with the absolute path
var fileProvider = new FileSystemFileProvider(absoluteBasePath);
//TODO: Configure your IoC to return the fileProvider instance when an IFileProvider is requested.
As you see in step 2 I read an application setting value from the web.config file. This should be present on appSettings section in your configuration file.
<appSettings>
<add key="fileBasePath" value="~/FileStorage"/>
</appSettings>
With all this in place you could inject your fileProvider to your controllers like this:
public class UploadController : Controller
{
private readonly IFileProvider fileProvider;
public UploadController(IFileProvider fileProvider)
{
this.fileProvider = fileProvider;
}
And then you can use your provider on the relevant action when need it.
[HttpPost]
public ActionResult Upload(FileUploadModel model)
{
//Validate
if (ModelState.IsValid)
{
//Use your fileProvider here to upload the file
var content = new byte[model.PostedFile.ContentLength];
model.PostedFile.InputStream.Write(content, 0, content.Length);
var result = fileProvider.UploadFile(content, model.PostedFile.FileName);
if (result.Success)
{
//TODO: Notify the user about operation success
return RedirectToAction("Index", "Upload");
}
}
//
return View(model);
}
Hope this helps!
PS: I don't know why code formatting is a little bit crazy.
I am trying to test that my base controller is decorated with a certain action filter. Because this filter's constructor looks to web.config, my first try at testing fails because the test project doesn't have a valid config file. Moving on, I used a TestConfigProvider that I inject into the filter constructor, but the following test fails because the config provider isn't passed to the constructor. How else can I test if this filter is applied?
[TestMethod]
public void Base_controller_must_have_MaxLengthFilter_attribute()
{
var att = typeof(BaseController).GetCustomAttribute<MaxLengthFilter>();
Assert.IsNotNull(att);
}
Well, you have taken a good first step by recognizing that Web.config is just another dependency and wrapping it into a ConfigProvider to inject is an excellent solution.
But, you are getting tripped up on one of the design problems of MVC - namely, that to be DI-friendly, attributes should only provide meta-data, but never actually define behavior. This isn't an issue with your approach to testing, it is an issue with the approach to the design of the filter.
As pointed out in the post, you can get around this issue by splitting your action filter attribute into 2 parts.
An attribute that contains no behavior to flag your controllers and action methods with.
A DI-friendly class that implements IActionFilter and contains the desired behavior.
The approach is to use the IActionFilter to test for the presence of the attribute, and then execute the desired behavior. The action filter can be supplied with all dependencies and then injected when the application is composed.
IConfigProvider provider = new WebConfigProvider();
IActionFilter filter = new MaxLengthActionFilter(provider);
GlobalFilters.Filters.Add(filter);
NOTE: If you need any of the filter's dependencies to have a lifetime shorter than singleton, you will need to use a GlobalFilterProvider as in this answer.
The implementation of MaxLengthActionFilter would look something like this:
public class MaxLengthActionFilter : IActionFilter
{
public readonly IConfigProvider configProvider;
public MaxLengthActionFilter(IConfigProvider configProvider)
{
if (configProvider == null)
throw new ArgumentNullException("configProvider");
this.configProvider = configProvider;
}
public void OnActionExecuted(ActionExecutedContext filterContext)
{
var attribute = this.GetMaxLengthAttribute(filterContext.ActionDescriptor);
if (attribute != null)
{
var maxLength = attribute.MaxLength;
// Execute your behavior here, and use the configProvider as needed
}
}
public void OnActionExecuting(ActionExecutingContext filterContext)
{
var attribute = this.GetMaxLengthAttribute(filterContext.ActionDescriptor);
if (attribute != null)
{
var maxLength = attribute.MaxLength;
// Execute your behavior here, and use the configProvider as needed
}
}
public MaxLengthAttribute GetMaxLengthAttribute(ActionDescriptor actionDescriptor)
{
MaxLengthAttribute result = null;
// Check if the attribute exists on the controller
result = (MaxLengthAttribute)actionDescriptor
.ControllerDescriptor
.GetCustomAttributes(typeof(MaxLengthAttribute), false)
.SingleOrDefault();
if (result != null)
{
return result;
}
// NOTE: You might need some additional logic to determine
// which attribute applies (or both apply)
// Check if the attribute exists on the action method
result = (MaxLengthAttribute)actionDescriptor
.GetCustomAttributes(typeof(MaxLengthAttribute), false)
.SingleOrDefault();
return result;
}
}
And, your attribute which should not contain any behavior should look something like this:
// This attribute should contain no behavior. No behavior, nothing needs to be injected.
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false)]
public class MaxLengthAttribute : Attribute
{
public MaxLengthAttribute(int maxLength)
{
this.MaxLength = maxLength;
}
public int MaxLength { get; private set; }
}
With a more loosely coupled design, testing for the existence of the attribute is much more straightforward.
[TestMethod]
public void Base_controller_must_have_MaxLengthFilter_attribute()
{
var att = typeof(BaseController).GetCustomAttribute<MaxLengthAttribute>();
Assert.IsNotNull(att);
}
Perhaps you can add the valid config file to your test project via "add file as link"
Recently I here more and more question regarding config "problems". They all have a common base - you have several projects, servers, services that need to use the same config. My advise to you - stop using Web.config.
Place all your configuration into the database!
Add a table (or maybe several tables) with all your configuration keys an values and read them when the application starts (global.asax).
This way you don't need to worry about coping your config to every project or injecting it to different constructors.
I'm attempting to create a framework for allowing controllers and views to be dynamically imported into an MVC application. Here's how it works so far:
I'm using .NET 4, ASP.NET MVC 3 RC and the Razor ViewEngine
Controllers are exported and imported using MEF per project - I call a set of controllers and views from a given project a "Module"
Assemblies discovered using MEF are dynamically referenced by the BuildManager using a pre-application start method and BuildManager.AddReferencedAssembly.
Binaries (from exporting project) and Views are copied into the target project's folder structure using a build event
Controllers are selected using a custom controller factory which inherits from DefaultControllerFactory and overrides GetControllerType()
Views are selected using a custom view engine which inherits from RazorViewEngine and overrides GetView() and GetPartialView() to allow it to look for views in Module-specific view directories
Everything works so far except for views using a strongly typed model. Views that use the dynamic model work fine, but when I specify a model type using #model, I get a YSOD that says "The view 'Index' or its master was not found".
When debugging my ViewEngine implementation, I can see that:
this.VirtualPathProvider.FileExists(String.Format(this.ViewLocationFormats[2], viewName, controllerContext.RouteData.GetRequiredString("controller"))) returns true, while
this.FileExists(controllerContext, String.Format(this.ViewLocationFormats[2], viewName, controllerContext.RouteData.GetRequiredString("controller"))) returns false.
Looking in Reflector, the RazorViewEngine implementation of FileExists() ultimately winds up doing this:
return (BuildManager.GetObjectFactory(virtualPath, false) != null);
However, I can't view BuildManager.GetObjectFactory() from Reflector because it's hidden somehow.
I'm suspecting that it has something to do with the fact that the model type is a type that is loaded from MEF, but since I'm already referencing the assemblies discovered by MEF from BuildManager, I'm out of leads. Can anyone provide a little more insight into what might be going on?
Update:
Turns out I was using an outdated version of Reflector from before .NET 4. I can see GetObjectFactory() now, but I can't really seem to find anything helpful. I've tried adding this into my FindView() overload:
try
{
var path = String.Format(this.ViewLocationFormats[2], viewName, controllerContext.RouteData.GetRequiredString("controller"));
var objFactory = System.Web.Compilation.BuildManager.GetObjectFactory(virtualPath: path, throwIfNotFound: true);
}
catch
{
}
Unfortunately, objFactory ends up null, and no exception gets thrown. All the bits that deal with compilation errors are part of private methods or types so I can't debug any of that, but it even seems like they'd end up throwing an exception, which doesn't seem to be happening. Looks like I'm at a dead end again. Help!
Update 2
I've discovered that at the point where FindView() is being called, if I call AppDomain.CurrentDomain.GetAssemblies(), the assembly that the model type is in is included. However, I cannot load the type using Type.GetType().
Update 3
Here's what I'm seeing:
Update 4
Here's the ViewEngine implementation:
using System;
using System.Linq;
using System.Web.Mvc;
using System.Web.Hosting;
using System.Web.Compilation;
namespace Site.Admin.Portal
{
public class ModuleViewEngine : RazorViewEngine
{
private static readonly String[] viewLocationFormats = new String[]
{
"~/Views/{0}/{{1}}/{{0}}.aspx",
"~/Views/{0}/{{1}}/{{0}}.ascx",
"~/Views/{0}/{{1}}/{{0}}.cshtml",
"~/Views/{0}/Shared/{{0}}.aspx",
"~/Views/{0}/Shared/{{0}}.ascx",
"~/Views/{0}/Shared/{{0}}.cshtml"
};
public ModuleViewEngine(IModule module)
{
this.Module = module;
var formats = viewLocationFormats.Select(f => String.Format(f, module.Name)).ToArray();
this.ViewLocationFormats = formats;
this.PartialViewLocationFormats = formats;
this.AreaViewLocationFormats = formats;
this.AreaPartialViewLocationFormats = formats;
this.AreaMasterLocationFormats = formats;
}
public IModule Module { get; private set; }
public override ViewEngineResult FindPartialView(ControllerContext controllerContext, String partialViewName, Boolean useCache)
{
var moduleName = controllerContext.RouteData.GetRequiredString("module");
if (moduleName.Equals(this.Module.Name, StringComparison.InvariantCultureIgnoreCase))
{
return base.FindPartialView(controllerContext, partialViewName, useCache);
}
else return new ViewEngineResult(new String[0]);
}
public override ViewEngineResult FindView(ControllerContext controllerContext, String viewName, String masterName, Boolean useCache)
{
var moduleName = controllerContext.RouteData.GetRequiredString("module");
if (moduleName.Equals(this.Module.Name, StringComparison.InvariantCultureIgnoreCase))
{
var baseResult = base.FindView(controllerContext, viewName, masterName, useCache);
return baseResult;
}
else return new ViewEngineResult(new String[0]);
}
}
}
Based on Update 2, I'm guessing what you've got is an explicitly loaded copy of your assembly (that is, it was loaded through some other method than Load, like LoadFrom). Explicitly loaded assemblies are set off aside into a special place, because they are not allowed to satisfy implicit type requirements. The rules for Fusion (the assembly loader) can be pretty arcane and hard to understand.
I agree with Matthew's assessment that, to get this to work, your DLL is going to have to be in /bin or else it will never be able to satisfy the implicit type requirement.
The imported libaries aren't in the /bin directory so aren't probed when trying to resolve references. I discovered a work around which I published in my MVC + MEF article (Part 2). Essentially you need to add your directories where your extensions sit to the probing path of the AppDomain.
Essentially where I am building my container:
/// <summary>
/// Creates the composition container.
/// </summary>
/// <returns></returns>
protected virtual CompositionContainer CreateCompositionContainer()
{
var catalog = new AggregateCatalog();
catalog.Catalogs.Add(new DirectoryCatalog(MapPath("~/bin")));
var config = CompositionConfigurationSection.GetInstance();
if (config != null && config.Catalogs != null) {
config.Catalogs
.Cast<CatalogConfigurationElement>()
.ForEach(c =>
{
if (!string.IsNullOrEmpty(c.Path)) {
string path = c.Path;
if (path.StartsWith("~"))
path = MapPath(path);
foreach (var directoryCatalog in GetDirectoryCatalogs(path)) {
// Register our path for probing.
RegisterPath(directoryCatalog.FullPath);
// Add the catalog.
catalog.Catalogs.Add(directoryCatalog);
}
}
});
}
var provider = new DynamicInstantiationExportProvider();
var container = new CompositionContainer(catalog, provider);
provider.SourceProvider = container;
return container;
}
I register all the directories of catalogs in the current domain:
/// <summary>
/// Registers the specified path for probing.
/// </summary>
/// <param name="path">The probable path.</param>
private void RegisterPath(string path)
{
AppDomain.CurrentDomain.AppendPrivatePath(path);
}
I believe the same should work for MVC3.
UPDATE: Correct me if I am wrong, but I don't believe that ViewEngines are instantiated once per request, you create a single instance that you register with MVC. Because of this only one IModule instance is ever used with your ViewEngine, so if a path doesn't match that first IModule.Name it won't be found? Does that make sense?
How can I have a view render a partial (user control) from a different folder?
With preview 3 I used to call RenderUserControl with the complete path, but whith upgrading to preview 5 this is not possible anymore.
Instead we got the RenderPartial method, but it's not offering me the functionality I'm looking for.
Just include the path to the view, with the file extension.
Razor:
#Html.Partial("~/Views/AnotherFolder/Messages.cshtml", ViewData.Model.Successes)
ASP.NET engine:
<% Html.RenderPartial("~/Views/AnotherFolder/Messages.ascx", ViewData.Model.Successes); %>
If that isn't your issue, could you please include your code that used to work with the RenderUserControl?
In my case I was using MvcMailer (https://github.com/smsohan/MvcMailer) and wanted to access a partial view from another folder, that wasn't in "Shared." The above solutions didn't work, but using a relative path did.
#Html.Partial("../MyViewFolder/Partials/_PartialView", Model.MyObject)
If you are using this other path a lot of the time you can fix this permanently without having to specify the path all of the time. By default, it is checking for partial views in the View folder and in the Shared folder. But say you want to add one.
Add a class to your Models folder:
public class NewViewEngine : RazorViewEngine {
private static readonly string[] NEW_PARTIAL_VIEW_FORMATS = new[] {
"~/Views/Foo/{0}.cshtml",
"~/Views/Shared/Bar/{0}.cshtml"
};
public NewViewEngine() {
// Keep existing locations in sync
base.PartialViewLocationFormats = base.PartialViewLocationFormats.Union(NEW_PARTIAL_VIEW_FORMATS).ToArray();
}
}
Then in your Global.asax.cs file, add the following line:
ViewEngines.Engines.Add(new NewViewEngine());
For readers using ASP.NET Core 2.1 or later and wanting to use Partial Tag Helper syntax, try this:
<partial name="~/Views/Folder/_PartialName.cshtml" />
The tilde (~) is optional.
The information at https://learn.microsoft.com/en-us/aspnet/core/mvc/views/partial?view=aspnetcore-3.1#partial-tag-helper is helpful too.
For a user control named myPartial.ascx located at Views/Account folder write like this:
<%Html.RenderPartial("~/Views/Account/myPartial.ascx");%>
I've created a workaround that seems to be working pretty well. I found the need to switch to the context of a different controller for action name lookup, view lookup, etc. To implement this, I created a new extension method for HtmlHelper:
public static IDisposable ControllerContextRegion(
this HtmlHelper html,
string controllerName)
{
return new ControllerContextRegion(html.ViewContext.RouteData, controllerName);
}
ControllerContextRegion is defined as:
internal class ControllerContextRegion : IDisposable
{
private readonly RouteData routeData;
private readonly string previousControllerName;
public ControllerContextRegion(RouteData routeData, string controllerName)
{
this.routeData = routeData;
this.previousControllerName = routeData.GetRequiredString("controller");
this.SetControllerName(controllerName);
}
public void Dispose()
{
this.SetControllerName(this.previousControllerName);
}
private void SetControllerName(string controllerName)
{
this.routeData.Values["controller"] = controllerName;
}
}
The way this is used within a view is as follows:
#using (Html.ControllerContextRegion("Foo")) {
// Html.Action, Html.Partial, etc. now looks things up as though
// FooController was our controller.
}
There may be unwanted side effects for this if your code requires the controller route component to not change, but in our code so far, there doesn't seem to be any negatives to this approach.
The VirtualPathProviderViewEngine, on which the WebFormsViewEngine is based, is supposed to support the "~" and "/" characters at the front of the path so your examples above should work.
I noticed your examples use the path "~/Account/myPartial.ascx", but you mentioned that your user control is in the Views/Account folder. Have you tried
<%Html.RenderPartial("~/Views/Account/myPartial.ascx");%>
or is that just a typo in your question?
you should try this
~/Views/Shared/parts/UMFview.ascx
place the ~/Views/ before your code
Create a Custom View Engine and have a method that returns a ViewEngineResult
In this example you just overwrite the _options.ViewLocationFormats and add your folder directory
:
public ViewEngineResult FindView(ActionContext context, string viewName, bool isMainPage)
{
var controllerName = context.GetNormalizedRouteValue(CONTROLLER_KEY);
var areaName = context.GetNormalizedRouteValue(AREA_KEY);
var checkedLocations = new List<string>();
foreach (var location in _options.ViewLocationFormats)
{
var view = string.Format(location, viewName, controllerName);
if (File.Exists(view))
{
return ViewEngineResult.Found("Default", new View(view, _ViewRendering));
}
checkedLocations.Add(view);
}
return ViewEngineResult.NotFound(viewName, checkedLocations);
}
Example: https://github.com/AspNetMonsters/pugzor
Try using RenderAction("myPartial","Account");