I am trying to implement a custom view engine with Razor. The goal is if the view is in a sub folder to use that view instead.
I have my view engine derived from the RazorViewEngine
public class RazorViewFactory : RazorViewEngine
{
public RazorViewFactory()
{
string TenantID = ConfigurationManager.AppSettings["TenantID"];
if (TenantID != null)
{
MasterLocationFormats = new[] {
"~/Views/Shared/{0}.cshtml"
};
ViewLocationFormats = new[]{
"~/Tenant/" + TenantID + "/Views/{1}/{0}.cshtml",
"~/Tenant/" + TenantID + "/Views/Shared/{0}.cshtml",
"~/Views/{1}/{0}.cshtml",
"~/Views/Shared/{0}.cshtml"
};
PartialViewLocationFormats = new[] {
"~/Tenant/" + TenantID + "/Views/{1}/{0}.cshtml",
"~/Tenant/" + TenantID + "/Views/Shared/{0}.cshtml"
};
}
}
}
and in my Global.asax
protected void Application_Start()
{
...
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new RazorViewFactory());
}
Everything works except when I load my Tenant sub view Home page, I get the following error.
The view at '~/Tenant/TenantB/Views/Home/Index.cshtml'
must derive from WebViewPage, or WebViewPage<TModel>.
If I load the base home page it works fine with the Razor engine.
You need to copy the web.config file from your Views folder into your Tenant folder (or make sure it has the same config sections as described here: Razor HtmlHelper Extensions (or other namespaces for views) Not Found)
Related
I have the following code in my Android app, it basically uses one page (using a NavigationDrawer) and swaps fragments in/out of the central view. This allows the navigation to occur on one page instead of many pages:
Setup.cs:
protected override IMvxAndroidViewPresenter CreateViewPresenter()
{
var customPresenter = new MvxFragmentsPresenter();
Mvx.RegisterSingleton<IMvxFragmentsPresenter>(customPresenter);
return customPresenter;
}
ShellPage.cs
public class ShellPage : MvxCachingFragmentCompatActivity<ShellPageViewModel>, IMvxFragmentHost
{
.
.
.
public bool Show(MvxViewModelRequest request, Bundle bundle)
{
if (request.ViewModelType == typeof(MenuContentViewModel))
{
ShowFragment(request.ViewModelType.Name, Resource.Id.navigation_frame, bundle);
return true;
}
else
{
ShowFragment(request.ViewModelType.Name, Resource.Id.content_frame, bundle, true);
return true;
}
}
public bool Close(IMvxViewModel viewModel)
{
CloseFragment(viewModel.GetType().Name, Resource.Id.content_frame);
return true;
}
.
.
.
}
How can I achieve the same behavior in a Windows UWP app? Or rather, is there ANY example that exists for a Windows MvvmCross app which implements a CustomPresenter? That may at least give me a start as to how to implement it.
Thanks!
UPDATE:
I'm finally starting to figure out how to go about this with a customer presenter:
public class CustomPresenter : IMvxWindowsViewPresenter
{
IMvxWindowsFrame _rootFrame;
public CustomPresenter(IMvxWindowsFrame rootFrame)
{
_rootFrame = rootFrame;
}
public void AddPresentationHintHandler<THint>(Func<THint, bool> action) where THint : MvxPresentationHint
{
throw new NotImplementedException();
}
public void ChangePresentation(MvxPresentationHint hint)
{
throw new NotImplementedException();
}
public void Show(MvxViewModelRequest request)
{
if (request.ViewModelType == typeof(ShellPageViewModel))
{
//_rootFrame?.Navigate(typeof(ShellPage), null); // throws an exception
((Frame)_rootFrame.UnderlyingControl).Content = new ShellPage();
}
}
}
When I try to do a navigation to the ShellPage, it fails. So when I set the Content to the ShellPage it works, but the ShellPage's ViewModel is not initialized automatically when I do it that way. I'm guessing ViewModels are initialized in MvvmCross using OnNavigatedTo ???
I ran into the same issue, and built a custom presenter for UWP. It loans a couple of ideas from an Android sample I found somewhere, which uses fragments. The idea is as follows.
I have a container view which can contain multiple sub-views with their own ViewModels. So I want to be able to present multiple views within the container.
Note: I'm using MvvmCross 4.0.0-beta3
Presenter
using System;
using Cirrious.CrossCore;
using Cirrious.CrossCore.Exceptions;
using Cirrious.MvvmCross.ViewModels;
using Cirrious.MvvmCross.Views;
using Cirrious.MvvmCross.WindowsUWP.Views;
using xxxxx.WinUniversal.Extensions;
namespace xxxxx.WinUniversal.Presenters
{
public class MvxWindowsMultiRegionViewPresenter
: MvxWindowsViewPresenter
{
private readonly IMvxWindowsFrame _rootFrame;
public MvxWindowsMultiRegionViewPresenter(IMvxWindowsFrame rootFrame)
: base(rootFrame)
{
_rootFrame = rootFrame;
}
public override async void Show(MvxViewModelRequest request)
{
var host = _rootFrame.Content as IMvxMultiRegionHost;
var view = CreateView(request);
if (host != null && view.HasRegionAttribute())
{
host.Show(view as MvxWindowsPage);
}
else
{
base.Show(request);
}
}
private static IMvxWindowsView CreateView(MvxViewModelRequest request)
{
var viewFinder = Mvx.Resolve<IMvxViewsContainer>();
var viewType = viewFinder.GetViewType(request.ViewModelType);
if (viewType == null)
throw new MvxException("View Type not found for " + request.ViewModelType);
// Create instance of view
var viewObject = Activator.CreateInstance(viewType);
if (viewObject == null)
throw new MvxException("View not loaded for " + viewType);
var view = viewObject as IMvxWindowsView;
if (view == null)
throw new MvxException("Loaded View is not a IMvxWindowsView " + viewType);
view.ViewModel = LoadViewModel(request);
return view;
}
private static IMvxViewModel LoadViewModel(MvxViewModelRequest request)
{
// Load the viewModel
var viewModelLoader = Mvx.Resolve<IMvxViewModelLoader>();
return viewModelLoader.LoadViewModel(request, null);
}
}
}
IMvxMultiRegionHost
using Cirrious.MvvmCross.ViewModels;
using Cirrious.MvvmCross.WindowsUWP.Views;
namespace xxxxx.WinUniversal.Presenters
{
public interface IMvxMultiRegionHost
{
void Show(MvxWindowsPage view);
void CloseViewModel(IMvxViewModel viewModel);
void CloseAll();
}
}
RegionAttribute
using System;
namespace xxxxx.WinUniversal.Presenters
{
[AttributeUsage(AttributeTargets.Class)]
public sealed class RegionAttribute
: Attribute
{
public RegionAttribute(string regionName)
{
Name = regionName;
}
public string Name { get; private set; }
}
}
These are the three foundational classes you need. Next you'll need to implement the IMvxMultiRegionHost in a MvxWindowsPage derived class.
This is the one I'm using:
HomeView.xaml.cs
using System;
using System.Diagnostics;
using System.Linq;
using Windows.Foundation;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Navigation;
using Cirrious.MvvmCross.ViewModels;
using Cirrious.MvvmCross.WindowsUWP.Views;
using xxxxx.Shared.Controls;
using xxxxx.WinUniversal.Extensions;
using xxxxx.WinUniversal.Presenters;
using xxxxx.Core.ViewModels;
namespace xxxxx.WinUniversal.Views
{
public partial class HomeView
: MvxWindowsPage
, IMvxMultiRegionHost
{
public HomeView()
{
InitializeComponent();
}
// ...
public void Show(MvxWindowsPage view)
{
if (!view.HasRegionAttribute())
throw new InvalidOperationException(
"View was expected to have a RegionAttribute, but none was specified.");
var regionName = view.GetRegionName();
RootSplitView.Content = view;
}
public void CloseViewModel(IMvxViewModel viewModel)
{
throw new NotImplementedException();
}
public void CloseAll()
{
throw new NotImplementedException();
}
}
}
The last piece to make this work is the way the actual xaml in the view is set-up. You'll notice that I'm using a SplitView control, and that I'm replacing the Content property with the new View that's coming in in the ShowView method on the HomeView class.
HomeView.xaml
<SplitView x:Name="RootSplitView"
DisplayMode="CompactInline"
IsPaneOpen="false"
CompactPaneLength="48"
OpenPaneLength="200">
<SplitView.Pane>
// Some ListView with menu items.
</SplitView.Pane>
<SplitView.Content>
// Initial content..
</SplitView.Content>
</SplitView>
EDIT:
Extension Methods
I forgot to post the two extension methods to determine if the view declares a [Region] attribute.
public static class RegionAttributeExtentionMethods
{
public static bool HasRegionAttribute(this IMvxWindowsView view)
{
var attributes = view
.GetType()
.GetCustomAttributes(typeof(RegionAttribute), true);
return attributes.Any();
}
public static string GetRegionName(this IMvxWindowsView view)
{
var attributes = view
.GetType()
.GetCustomAttributes(typeof(RegionAttribute), true);
if (!attributes.Any())
throw new InvalidOperationException("The IMvxView has no region attribute.");
return ((RegionAttribute)attributes.First()).Name;
}
}
Hope this helps.
As the link to the blog of #Stephanvs is no longer active I was able to pull the content off the Web Archive, i'll post it here for who ever is looking for it:
Implementing a Multi Region Presenter for Windows 10 UWP and MvvmCross
18 October 2015 on MvvmCross, Xamarin, UWP, Windows 10, Presenter > Universal Windows Platform
I'm upgrading a Windows Store app to the new Windows 10 Universal
Windows Platform. MvvmCross has added support for UWP in v4.0-beta2.
A new control in the UWP is the SplitView control. Basically it
functions as a container view which consist of two sub views, shown
side-by-side. Mostly it's used to implement the (in)famous hamburger
menu.
By default MvvmCross doesn't know how to deal with the SplitView, and
just replaces the entire screen contents with a new View when
navigating between ViewModels. If however we want to lay-out our views
differently and show multiple views within one window, we need a
different solution. Luckily we can plug-in a custom presenter, which
will take care of handling the lay-out per platform.
Registering the MultiRegionPresenter
In the Setup.cs file in your UWP project, you can override the
CreateViewPresenter method with the following implementation.
protected override IMvxWindowsViewPresenter CreateViewPresenter(IMvxWindowsFrame rootFrame)
{
return new MvxWindowsMultiRegionViewPresenter(rootFrame);
}
Using Regions
We can define a region by declaring a
element. At this point it has to be a Frame type because then we can
also show a nice transition animation when switching views.
<mvx:MvxWindowsPage ...>
<Grid>
<!-- ... -->
<SplitView>
<SplitView.Pane>
<!-- Menu Content as ListView or something similar -->
</SplitView.Pane>
<SplitView.Content>
<Frame x:Name="MainContent" />
</SplitView.Content>
</SplitView>
</Grid>
</mvx:MvxWindowsPage>
Now we want to be able when a ShowViewModel(...) occurs to swap out
the current view presented in the MainContent frame.
Showing Views in a Region
In the code-behind for a View we can now declare a MvxRegionAttribute,
defining in which region we want this View to be rendered. This name
has to match a Frame element in the view.
[MvxRegion("MainContent")]
public partial class PersonView
{
// ...
}
It's also possible to declare multiple regions within the same view.
This would allow you to split up your UI in more re-usable pieces.
Animating the Transition between Content Views
If you want a nice animation when transitioning between views in the
Frame, you can add the following snippet to the Frame declaration.
<Frame x:Name="MainContent">
<Frame.ContentTransitions>
<TransitionCollection>
<NavigationThemeTransition>
<NavigationThemeTransition.DefaultNavigationTransitionInfo>
<EntranceNavigationTransitionInfo />
</NavigationThemeTransition.DefaultNavigationTransitionInfo>
</NavigationThemeTransition>
</TransitionCollection>
</Frame.ContentTransitions>
</Frame>
The contents will now be nicely animated when navigating.
Hope this helps, Stephanvs
I followed this tutorial, and created this code:
using Glass.Sitecore.Mapper;
using Sitecore.Mvc.Controllers;
using Sitecore.SecurityModel;
using SitecoreCMSMVCBase.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace SitecoreCMSMVCBase.Controllers
{
public class CommentController : SitecoreController
{
ISitecoreContext _context;
ISitecoreService _master;
public CommentController()
: this(
new SitecoreContext(),
new SitecoreService("master"))
{
}
/// <summary>
/// This constructor can be used with dependency injection or unit testing
/// </summary>
public CommentController(ISitecoreContext context, ISitecoreService master)
{
_context = context;
_master = master;
}
[HttpGet]
public override ActionResult Index()
{
var model = _context.GetCurrentItem<CommentPage>();
return View(model);
}
[HttpPost]
public ActionResult Index(Comment comment)
{
var webModel = _context.GetCurrentItem<CommentPage>();
if (ModelState.IsValid)
{
var masterModel = _master.GetItem<CommentPage>(webModel.Id);
if (masterModel.CommentFolder == null)
{
CommentFolder folder = new CommentFolder();
folder.Name = "Comments";
using (new SecurityDisabler())
{
_context.Create(masterModel, folder);
}
masterModel.CommentFolder = folder;
}
using (new SecurityDisabler())
{
comment.Name = DateTime.Now.ToString("yyyyMMddhhmmss");
//create the comment in the master database
_master.Create(masterModel.CommentFolder, comment);
webModel.CommentAdded = true;
}
}
return View(webModel);
}
}
}
Models are identical with tutorial, so I will not paste them.
My route configuration looks like this:
routes.MapRoute(
"CommentController", // Route name
"Comment/{action}/{id}", // URL with parameters
new { controller = "Comment", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
When I navigate to /comment I see this exception:
Glass.Sitecore.Mapper.MapperException: Context has not been loaded
I tried with commenting my route specification (as there was nothing about routes in tutorial), and then error is different (throwing by Sitecore CMS itself):
The requested document was not found
Do you know how to load Sitecore context into custom Controller, and make this simple example work? I was looking everywhere but couldn't find any good answer...
I think this is more a Glass setup issue, rather than an MVC routing problem.
To setup Glass, you need to initialise the context in your application start method in your Global.asax file.
var loader = new Glass.Sitecore.Mapper.Configuration.Attributes.AttributeConfigurationLoader(
"Glass.Sitecore.Mapper.Tutorial.Models, Glass.Sitecore.Mapper.Tutorial");
Glass.Sitecore.Mapper.Context context = new Context(loader);
For other Glass-setup related stuff I recommend following the first tutorial on the glass.lu website.
http://www.glass.lu/tutorials/glass-sitecore-mapper-tutorials/tutorial-1-setup/
This method doesn't need Glass at all!
First step is to set your route in Global.asax file.
routes.MapRoute(
"DemoController", // Route name
"Demo/{action}/{param}", // URL with parameters
new { controller = "Demo", action = "Index", param = "", scItemPath = "/sitecore/content/DemoHomePage" } // Parameter defaults
);
Notice that controller is not taken as parameter, but is fixed, to prevent handling it by Sitecore. More info here and here. Notice that there is one additional parameter - scItemPath. It contains path to item which by default will be included in page context.
Having this route our traffic from /demo is handled by DemoController and Index action. Inside this action all you need is to add is this line:
Sitecore.Data.Items.Item item = Sitecore.Mvc.Presentation.PageContext.Current.Item;
item variable will contain your Sitecore item pointed by scItemPath.
And that's all - it should work well now - hope it helps!
I have a controller action method that needs to be able to serve multiple views. These views are generated by XSLT.
Now, the views have images in them (hundreds each), and each view needs to have its own folder with images to refer to. How should this work?
If the images in the source XML has an href that is a simple relative path ("images/image.svg"), how can I get this path to resolve in the view in the application?
If I could put the images folder in the same folder as the view, and use a relative path there, it would be easy, but that doesn't work, because I'm serving multiple views from the action. Here is the routing:
routes.MapRoute(
"Parameter",
"{controller}/{action}/{lang}/{prod}",
new { controller = "Manuals", action = "Product", lang = "en-US", prod = "sample" }
);
So if I try using a relative path for the img src attribute, it resolves to something like "/Manuals/Product/en-US/images/image.svg"
And in fact, if I put it relative to the view, the image is located in "/Views/Manuals/en-US/images/image.svg"
So is there no way to have relative image paths like this in Asp.Net MVC? Or am I misunderstanding MVC routing completely?
This is what I have done before:
public class MvcApplication : HttpApplication
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new HandleErrorAttribute());
}
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
MapRoute(routes, "", "Home", "Index");
/* other routes */
MapRoute(routes, "{*url}", "Documentation", "Render");
}
}
Now any routes that are not matched are passed to the DocumentationController. My documentation controller looks as follows:
public class DocumentationController : Controller
{
public ActionResult Render(string url)
{
var md = new MarkdownSharp.Markdown();
// The path is relative to the root of the application, but it can be anything
// stored on a different drive.
string path = Path.Combine(Request.MapPath("~/"), GetAppRelativePath().Replace('/', '\\')) + ".md";
if (System.IO.File.Exists(path))
{
string html = md.Transform(System.IO.File.ReadAllText(path));
return View("Render", (object)html);
}
// return the not found view if the file doesn't exist
return View("NotFound");
}
private string GetAppRelativePath()
{
return HttpContext.Request.AppRelativeCurrentExecutionFilePath.Replace("~/", "");
}
}
All this does is to find markdown files and render them accordingly. To update this for your case, you may want to do the following:
routes.MapRoute(
"Parameter1",
"{controller}/{action}/{lang}/{*url}",
new { controller = "Manuals", action = "Download", lang = "en-US", prod = "sample" }
);
Make sure it is after the {controller}/{action}/{lang}/{prod} route. This should cause a URL such as /Manuals/Product/en-US/images/image.svg or even images/image.svg (if the browser is in /Manuals/Product/en-US/sample to invoke the the Download action. You can then adapt the code I wrote to map that URI to the physical location. A problem you may run into is that "images" are considered to be product and that /Manuals/Product/en-US/images would think its a product.
The Images action can be can look as follows.
public ActionResult Download(string url)
{
/* figure out physical path */
var filename = /* get filename form url */
var fileStream = [...];
Response.Headers.Remove("Content-Disposition");
Response.Headers.Add("Content-Disposition", "inline; filename=" + filename);
string contentType = "image/jpg";
return File(fileStream, contentType, filename);
}
You can get more information of the FileResult at MSDN.
I have "widgets" contained within partial views and iterate over these files to build an available widget list. Within these views there are two variables
#{ ViewBag.Name = "Test Widget"; }
#{ ViewBag.Description = "This is a test widget"; }
self explanatory. Is there a way using RazorEngine (or any other way) to "read" these variables out so I can show the value of ViewBag.Description in my widget list?
Thanks.
RazorEngine with a custom base view could be used for this purpose:
public class MyViewModel
{
public string Name { get; set; }
public string Description { get; set; }
}
class Program
{
static void Main()
{
// don't ask me about this line :-)
// it's used to ensure that the Microsoft.CSharp assembly is loaded
bool loaded = typeof(Binder).Assembly != null;
Razor.SetTemplateBase(typeof(FooTemplateBase<>));
string template =
#"#{ ViewBag.Name = ""Test Widget""; }
#{ ViewBag.Description = ""This is a test widget""; }";
var model = new MyViewModel();
var result = Razor.Parse(template, model);
Console.WriteLine(model.Name);
Console.WriteLine(model.Description);
}
}
namespace System.Web.Mvc
{
public abstract class FooTemplateBase<T> : TemplateBase<T>
{
public dynamic ViewBag
{
get
{
return Model;
}
}
}
}
You can use the RazorGenerator to actually compile the views, then do a sample execution of them to be able to read the data.
See this article on unit testing the razor views, a similar thing may work for you:
http://blog.davidebbo.com/2011/06/unit-test-your-mvc-views-using-razor.html
var view = new Index();
// Set up the data that needs to be accessed by the view
view.ViewBag.Message = "Testing";
// Render it in an HtmlAgilityPack HtmlDocument. Note that
// you can pass a 'model' object here if your view needs one.
// Generally, what you do here is similar to how a controller
//action sets up data for its view.
HtmlDocument doc = view.RenderAsHtml();
//recall name and description
var name = view.ViewBag.Name;
var description = view.ViewBag.Description;
Here is some additional info on the Razor Generator from Phil Haack: http://haacked.com/archive/2011/08/01/text-templating-using-razor-the-easy-way.aspx
Anyone got the areaDescriptorFilter working with the spark view engine in asp.net mvc 2?
I don't even have the option to add a filter on the service as shown in the following:
http://sparkviewengine.com/documentation/viewlocations#Extendingfilepatternswithdescriptorfilters
Thanks if you can help or at least try.
I'm using areas with Spark in a project of mine. All I had to do was add AreaRegistration classes for each area like:
public class AdminAreaRegistration : System.Web.Mvc.AreaRegistration
{
public override string AreaName
{
get { return "Admin"; }
}
public override void RegisterArea( AreaRegistrationContext context )
{
context.MapRoute(
"Admin_default",
"Admin/{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
and then in the global.asax call:
AreaRegistration.RegisterAllAreas();
I have my area views located in a folder named "Admin" under the default "Views" folder, with appropriate controller folders under that:
\MvcProject
\Views
\Admin
\Home
\Index.spark
\Users
\Index.spark
from the page you linked:
the AreaDescriptorFilter is added by default
so you shouldn't need to worry about adding it yourself.