I have a project in MVC2, I have converted it to MVC3 then I have converted it to MVC4, I have converted aspx views to Razor Views using this tool,
I also installed MVC3 and MVC4, after converting my application compiled successfully,But when I run the application its showing error
Unable to cast object of type 'Microsoft.Web.Mvc.FixedRazorViewEngine'
to type 'System.Web.Mvc.WebFormViewEngine'.
In Global.asax
private static void AddViewPaths()
{
foreach (var engine in ViewEngines.Engines)
{
((WebFormViewEngine)engine).PartialViewLocationFormats =
((WebFormViewEngine)engine).PartialViewLocationFormats.Union(
new string[] { "~/Views/Sample/{0}.aspx", "~/Views/Sample/{0}.ascx" }).ToArray();
((WebFormViewEngine)engine).ViewLocationFormats =
((WebFormViewEngine)engine).ViewLocationFormats.Union(
new string[] { "~/Views/Sample/{0}.aspx", "~/Views/Sample/{0}.ascx" }).ToArray();
}
}
I tried changing these lines to this,but still same issue,what should I do?
((WebFormViewEngine)engine).PartialViewLocationFormats =
((WebFormViewEngine)engine).PartialViewLocationFormats.Union(
new string[] { "~/Views/Sample/{0}.cshtml", "~/Views/Sample/{0}.cshtml" }).ToArray();
((WebFormViewEngine)engine).ViewLocationFormats =
((WebFormViewEngine)engine).ViewLocationFormats.Union(
new string[] { "~/Views/Sample/{0}.cshtml", "~/Views/Sample/{0}.cshtml" }).ToArray();
The problem is I am having both Razor and WebForms in ViewEngines.Engines Collection,How can I only get Razor Views
Since you have moved to razor (which was not available in MVC 2), you now have more than just the WebFormsViewEngine available in your for loop. Your code, as written now, it attempting to cast a Razor engine to a WebForms engine, which cannot be done. If you update the hard casting and remove all other engines, it should work and you can get rid of the for loop - you only have one engine in your collection.
Application_Start
//clear all but RazorViewEngine
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new RazorViewEngine());
Now your updated code would simply be
var engine = ViewEngines.Engines.First();
((RazorViewEngine)engine).PartialViewLocationFormats =
((RazorViewEngine)engine).PartialViewLocationFormats.Union(
new string[] { "~/Views/Sample/{0}.cshtml", "~/Views/Sample/{0}.cshtml" }).ToArray();
((RazorViewEngine)engine).ViewLocationFormats =
((RazorViewEngine)engine).ViewLocationFormats.Union(
new string[] { "~/Views/Sample/{0}.cshtml", "~/Views/Sample/{0}.cshtml" }).ToArray();
If you decide not to remove all of the engines, then you are going to have to do an if/else inside of your for loop to determine which way to cast the engine.
Related
I'm developing a library for use in ASP.NET Core MVC. For this library, I need to get a list of all (compiled) Razor views (path and content) that exist in the application.
Is there a way to lookup all compiled Razor views in the application during runtime? So far I have not had any luck trying to find out.
Iterating through .cshtml files is no option because they will not be published when using compiled views.
For getting complied views path, you could try ViewsFeature like
public class HomeController : Controller
{
private readonly IViewCompilerProvider _viewCompilerProvider;
private readonly ApplicationPartManager _applicationPartManager;
public HomeController(IViewCompilerProvider viewCompilerProvider
, ApplicationPartManager applicationPartManager)
{
_viewCompilerProvider = viewCompilerProvider;
_applicationPartManager = applicationPartManager;
}
public IActionResult Index()
{
var feature = new ViewsFeature();
_applicationPartManager.PopulateFeature(feature);
var views = feature.ViewDescriptors;
return View();
}
}
I have a very simple test in a test project in a solution using ASP MVC V5 and attribute routing. Attribute routing and the MapMvcAttributeRoutes method are part of ASP MVC 5.
[Test]
public void HasRoutesInTable()
{
var routes = new RouteCollection();
routes.MapMvcAttributeRoutes();
Assert.That(routes.Count, Is.GreaterThan(0));
}
This results in:
System.InvalidOperationException :
This method cannot be called during the applications pre-start initialization phase.
Most of the answers to this error message involve configuring membership providers in the web.config file. This project has neither membership providers or a web.config file so the error seems be be occurring for some other reason. How do I move the code out of this "pre-start" state so that the tests can run?
The equivalent code for attributes on ApiController works fine after HttpConfiguration.EnsureInitialized() is called.
I recently upgraded my project to ASP.NET MVC 5 and experienced the exact same issue. When using dotPeek to investigate it, I discovered that there is an internal MapMvcAttributeRoutes extension method that has a IEnumerable<Type> as a parameter which expects a list of controller types. I created a new extension method that uses reflection and allows me to test my attribute-based routes:
public static class RouteCollectionExtensions
{
public static void MapMvcAttributeRoutesForTesting(this RouteCollection routes)
{
var controllers = (from t in typeof(HomeController).Assembly.GetExportedTypes()
where
t != null &&
t.IsPublic &&
t.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase) &&
!t.IsAbstract &&
typeof(IController).IsAssignableFrom(t)
select t).ToList();
var mapMvcAttributeRoutesMethod = typeof(RouteCollectionAttributeRoutingExtensions)
.GetMethod(
"MapMvcAttributeRoutes",
BindingFlags.NonPublic | BindingFlags.Static,
null,
new Type[] { typeof(RouteCollection), typeof(IEnumerable<Type>) },
null);
mapMvcAttributeRoutesMethod.Invoke(null, new object[] { routes, controllers });
}
}
And here is how I use it:
public class HomeControllerRouteTests
{
[Fact]
public void RequestTo_Root_ShouldMapTo_HomeIndex()
{
// Arrange
var routes = new RouteCollection();
// Act - registers traditional routes and the new attribute-defined routes
RouteConfig.RegisterRoutes(routes);
routes.MapMvcAttributeRoutesForTesting();
// Assert - uses MvcRouteTester to test specific routes
routes.ShouldMap("~/").To<HomeController>(x => x.Index());
}
}
One problem now is that inside RouteConfig.RegisterRoutes(route) I cannot call routes.MapMvcAttributeRoutes() so I moved that call to my Global.asax file instead.
Another concern is that this solution is potentially fragile since the above method in RouteCollectionAttributeRoutingExtensions is internal and could be removed at any time. A proactive approach would be to check to see if the mapMvcAttributeRoutesMethod variable is null and provide an appropriate error/exceptionmessage if it is.
NOTE: This only works with ASP.NET MVC 5.0. There were significant changes to attribute routing in ASP.NET MVC 5.1 and the mapMvcAttributeRoutesMethod method was moved to an internal class.
In ASP.NET MVC 5.1 this functionality was moved into its own class called AttributeRoutingMapper.
(This is why one shouldn't rely on code hacking around in internal classes)
But this is the workaround for 5.1 (and up?):
public static void MapMvcAttributeRoutes(this RouteCollection routeCollection, Assembly controllerAssembly)
{
var controllerTypes = (from type in controllerAssembly.GetExportedTypes()
where
type != null && type.IsPublic
&& type.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase)
&& !type.IsAbstract && typeof(IController).IsAssignableFrom(type)
select type).ToList();
var attributeRoutingAssembly = typeof(RouteCollectionAttributeRoutingExtensions).Assembly;
var attributeRoutingMapperType =
attributeRoutingAssembly.GetType("System.Web.Mvc.Routing.AttributeRoutingMapper");
var mapAttributeRoutesMethod = attributeRoutingMapperType.GetMethod(
"MapAttributeRoutes",
BindingFlags.Public | BindingFlags.Static,
null,
new[] { typeof(RouteCollection), typeof(IEnumerable<Type>) },
null);
mapAttributeRoutesMethod.Invoke(null, new object[] { routeCollection, controllerTypes });
}
Well, it's really ugly and I'm not sure if it'll be worth the test complexity, but here's how you can do it without modifying your RouteConfig.Register code:
[TestClass]
public class MyTestClass
{
[TestMethod]
public void MyTestMethod()
{
// Move all files needed for this test into a subdirectory named bin.
Directory.CreateDirectory("bin");
foreach (var file in Directory.EnumerateFiles("."))
{
File.Copy(file, "bin\\" + file, overwrite: true);
}
// Create a new ASP.NET host for this directory (with all the binaries under the bin subdirectory); get a Remoting proxy to that app domain.
RouteProxy proxy = (RouteProxy)ApplicationHost.CreateApplicationHost(typeof(RouteProxy), "/", Environment.CurrentDirectory);
// Call into the other app domain to run route registration and get back the route count.
int count = proxy.RegisterRoutesAndGetCount();
Assert.IsTrue(count > 0);
}
private class RouteProxy : MarshalByRefObject
{
public int RegisterRoutesAndGetCount()
{
RouteCollection routes = new RouteCollection();
RouteConfig.RegisterRoutes(routes); // or just call routes.MapMvcAttributeRoutes() if that's what you want, though I'm not sure why you'd re-test the framework code.
return routes.Count;
}
}
}
Mapping attribute routes needs to find all the controllers you're using to get their attributes, which requires accessing the build manager, which only apparently works in app domains created for ASP.NET.
What are you testing here? Looks like you are testing a 3rd party extension method. You shouldn't be using your unit tests to test 3rd party code.
In our project we're using the razorgenerator of David Ebbo. This allowed us to move some of our cshtml files to a class library.
What we would like to achieve now is the following:
MyCommonViews has a "MyView.cshtml" in its Views folder.
MyWebProject ALSO has a "MyView.cshtml" in its Views folder.
MyOtherWebProject DOES NOT have a "MyView.cshtml" in its Views folder.
When MyOtherWebProject needs to load MyView.cshtml, it will pick the one which is in the compiled MyCommonViews project. That is what we want.
BUT when MyWebProject needs to load MyView.cshtml, we would like it to pick up the "overridden" MyView.cshtml file which is in the MyWebProject itself.
Is what we want possible and how?
Manu.
I wrote up a hacky solution for our problem. It hacks into the razorgenerators viewengine and removes all appropriate entries from the (private readonly) Dictionary it has.
The code is ran on application start.
Talk is cheap, show me the code:
private static void HackRazorGeneratorToAllowViewOverriding()
{
// first we search for the PrecompiledMvcEngine
var razorGeneratorViewEngine = ViewEngines.Engines.ToList().FirstOrDefault(ve => ve.GetType().Name.Contains("PrecompiledMvcEngine"));
if (razorGeneratorViewEngine == null)
return;
// retrieve the dictionary where it keeps the mapping between a view path and the (view object) type to instantiate
var razorMappings = (IDictionary<string, Type>)ReflectionUtils.GetPrivateReadonly("_mappings", razorGeneratorViewEngine);
// retrieve a list of all our cshtml files in our 'concrete' web project
var files = Directory.GetFiles(Path.Combine(WebConfigSettings.RootPath, "Views"), "*.cshtml", SearchOption.AllDirectories);
// do some kungfu on those file paths so that they are in the same format as in the razor mapping dictionary
var concreteViewPaths = files.Select(fp => string.Format("~{0}", fp.Replace(WebConfigSettings.RootPath, "").Replace(#"\", "/"))).ToList();
// loop through each of the cshtml paths (of our 'concrete' project) and remove it from the razor mappings if it's there
concreteViewPaths.ForEach(vp =>
{
if (razorMappings.ContainsKey(vp))
razorMappings.Remove(vp);
});
}
WebConfigSettings.RootPath contains the path on HD to the root of our web application.
This is a part of our static ReflectionUtils class:
/// <summary>
/// Get a field that is 'private readonly'.
/// </summary>
public static object GetPrivateReadonly(string readonlyPropName, object instance)
{
var field = instance.GetType().GetField(readonlyPropName, BindingFlags.Instance | BindingFlags.NonPublic);
if (field == null)
throw new NullReferenceException(string.Format("private readonly field '{0}' not found in '{1}'", readonlyPropName, instance));
return field.GetValue(instance);
}
This did the trick. We basically force the PrecompiledMvcEngine to "forget" any view which we have in our concrete project.
You can also try CompositePrecompiledMvcEngine from RazorGenerator.Mvc 2.1.0. It was designed for correct support of view overriding within multiple assemblies. Piece of code:
var engine = new CompositePrecompiledMvcEngine(
/*1*/ PrecompiledViewAssembly.OfType<MyCommonViewsSomeClass>(),
/*2*/ PrecompiledViewAssembly.OfType<MyWebProjectSomeClass>(
usePhysicalViewsIfNewer: HttpContext.Current.IsDebuggingEnabled));
ViewEngines.Engines.Insert(0, engine);
VirtualPathFactoryManager.RegisterVirtualPathFactory(engine);
The first line will register all views from the MyCommonViews assembly (~/Views/MyView.cshtml), the second line will register all views from the MyWebProject or MyOtherWebProject assembly.
When it encounters the virtual path, that already has been registered (~/Views/MyView.cshtml from the MyWebProject assembly), it overrides an old mapping with a new view type mapping.
If another project doesn't has view with the same virtual path (MyOtherWebProject) it leaves source mapping unchanged.
There is flag PreemptPhysicalFiles = false which does the magic.
Full sample:
[assembly: WebActivator.PostApplicationStartMethod(typeof(Application.Web.Common.App_Start.RazorGeneratorMvcStart), "Start")]
namespace Application.Web.Common.App_Start
{
public static class RazorGeneratorMvcStart
{
public static void Start()
{
var engine = new PrecompiledMvcEngine2(typeof (RazorGeneratorMvcStart).Assembly)
{
UsePhysicalViewsIfNewer = true, //compile if file changed
PreemptPhysicalFiles = false //use local file if exist
};
ViewEngines.Engines.Add(engine);//Insert(0,engine) ignores local partial views
// StartPage lookups are done by WebPages.
VirtualPathFactoryManager.RegisterVirtualPathFactory(engine);
}
}
}
However there is maybe a small bug:
http://razorgenerator.codeplex.com/workitem/100
Would it be a good idea, and executable, to use the ASP.NET MVC View Engine to render html to be sent out by email?
I know it's possible to let the view render into a string. So that could be use to build the mail message.
Since ASP.NET MVC is already used in the application, I get to use all practical ASP.NET MVC stuff without having to use Brail or NVelocity for my "mail views".
Good idea? Any caveats? Examples :) ?
Yes it is a good idea and relatively easy to implement.
I personally think it's a good idea. Definitely better than putting a piece of markup with placeholders into the database.
The disadvantage is that you will need Visual Studio to edit those templates then recompile and redeploy the project. You wouldn't be able to "outsource" working with templates to other non-technical staff.
And yes, adding new templates would also require your personal intervention.
Here,s my version of the RenderPartialToString as an extension method (which also takes care of paths etc..):
public static class ExtensionMethods
{
public static string RenderPartialToString(this ControllerBase controller, string partialName, object model)
{
var vd = new ViewDataDictionary(controller.ViewData);
var vp = new ViewPage
{
ViewData = vd,
ViewContext = new ViewContext(),
Url = new UrlHelper(controller.ControllerContext.RequestContext)
};
ViewEngineResult result = ViewEngines
.Engines
.FindPartialView(controller.ControllerContext, partialName);
if (result.View == null)
{
throw new InvalidOperationException(
string.Format("The partial view '{0}' could not be found", partialName));
}
var partialPath = ((WebFormView)result.View).ViewPath;
vp.ViewData.Model = model;
Control control = vp.LoadControl(partialPath);
vp.Controls.Add(control);
var sb = new StringBuilder();
using (var sw = new StringWriter(sb))
{
using (var tw = new HtmlTextWriter(sw))
{
vp.RenderControl(tw);
}
}
return sb.ToString();
}
}
usage:
return this.RenderPartialToString("YourPartialView", yourModel);
hope this helps..
jim
You can use MVCMailer NuGet - it uses the MVC view templates and you just write a single line of code to do this!
I'm looking for a method of storing routing information in my web.config file in addition to the Global.asax class. The routes stored in the configuration file would need to take higher precedence than those added programmatically.
I've done my searching, but the closest I can come up with is the RouteBuilder on Codeplex (http://www.codeplex.com/RouteBuilder), but this doesn't work with the RTM version of MVC. Does a solution out there exist compatible with the final 1.0?
I can't guarantee the following code will work, but it builds :) Change the Init method in RouteBuilder.cs to the following:
public void Init(HttpApplication application)
{
// Grab the Routes from Web.config
RouteConfiguration routeConfig =
(RouteConfiguration)System.Configuration.ConfigurationManager.GetSection("RouteTable");
// Add each Route to RouteTable
foreach (RouteElement routeElement in routeConfig.Routes)
{
RouteValueDictionary defaults = new RouteValueDictionary();
string[] defaultsArray = routeElement.Defaults.Trim().Split(',');
if (defaultsArray.Length > 0)
{
foreach (string defaultEntry in defaultsArray)
{
string[] defaultsEntryArray = defaultEntry.Trim().Split('=');
if ((defaultsEntryArray.Length % 2) != 0)
{
throw new ArgumentException("RouteBuilder: All Keys in Defaults must have values!");
}
else
{
defaults.Add(defaultsEntryArray[0], defaultsEntryArray[1]);
}
}
}
else
{
throw new ArgumentException("RouteBuilder: Defaults value is empty or malformed.");
}
Route currentRoute = new Route(routeElement.Url, defaults, new MvcRouteHandler());
RouteTable.Routes.Add(currentRoute);
}
}
Also, feel free to delete the DefaultsType class. It was needed because the defaults system was much more complicated back in CTP than in RTM.
Edit: Oh and add using System.Web.Routing; to the top and make sure you add System.Web.Mvc and System.Web.Routing as references.
look at this:
http://weblogs.asp.net/fredriknormen/archive/2008/03/11/asp-net-mvc-framework-2-define-routes-in-web-config.aspx