I'm looking to use NVelocity in my ASP.NET MVC application, not as a view engine, just for rendering some email templates.
However, I cannot for the life of me get it to work. I have downloaded it from the castle project and followed the example at http://www.castleproject.org/others/nvelocity/usingit.html#step1
No matter what I try I don't seem to be able to load a template located in my site. The example suggests using the absolute path, which I have tried to no avail:
Template t = engine.GetTemplate("/Templates/TestEmail.vm");
So please can someone give me two examples. One of loading a template located in the web site directory and secondly one parsing a string variable (as it is likely that my templates will be stored in a database).
Many thanks
Ben
I've used this class in one of my past projects:
public interface ITemplateRepository
{
string RenderTemplate(string templateName, IDictionary<string, object> data);
string RenderTemplate(string masterPage, string templateName, IDictionary<string, object> data);
}
public class NVelocityTemplateRepository : ITemplateRepository
{
private readonly string _templatesPath;
public NVelocityTemplateRepository(string templatesPath)
{
_templatesPath = templatesPath;
}
public string RenderTemplate(string templateName, IDictionary<string, object> data)
{
return RenderTemplate(null, templateName, data);
}
public string RenderTemplate(string masterPage, string templateName, IDictionary<string, object> data)
{
if (string.IsNullOrEmpty(templateName))
{
throw new ArgumentException("The \"templateName\" parameter must be specified", "templateName");
}
var name = !string.IsNullOrEmpty(masterPage)
? masterPage : templateName;
var engine = new VelocityEngine();
var props = new ExtendedProperties();
props.AddProperty(RuntimeConstants.FILE_RESOURCE_LOADER_PATH, _templatesPath);
engine.Init(props);
var template = engine.GetTemplate(name);
template.Encoding = Encoding.UTF8.BodyName;
var context = new VelocityContext();
var templateData = data ?? new Dictionary<string, object>();
foreach (var key in templateData.Keys)
{
context.Put(key, templateData[key]);
}
if (!string.IsNullOrEmpty(masterPage))
{
context.Put("childContent", templateName);
}
using (var writer = new StringWriter())
{
engine.MergeTemplate(name, context, writer);
return writer.GetStringBuilder().ToString();
}
}
}
In order to instantiate the NVelocityTemplateRepository class you need to provide an absolute path where your templates root is. Then you use relative paths to reference your vm files.
I also added the following method to process a string instead of a template file (say if retrieving the template content from a database):
public string RenderTemplateContent(string templateContent, IDictionary<string, object> data)
{
if (string.IsNullOrEmpty(templateContent))
throw new ArgumentException("Template content cannot be null", "templateContent");
var engine = new VelocityEngine();
engine.Init();
var context = GetContext(data);
using (var writer = new StringWriter()) {
engine.Evaluate(context, writer, "", templateContent);
return writer.GetStringBuilder().ToString();
}
}
And used StructureMap to initialize the service:
ForRequestedType<ITemplateService>()
.TheDefault.Is.ConstructedBy(()=>
new NVelocityTemplateService(HttpContext.Current.Server.MapPath("~/Content/Templates/")));
You might find the TemplateEngine component useful.
It's an abstraction over template engines with a NVelocity implementation, similar to Darin's answer, but it should perform marginally better since it uses a single instance of the VelocityEngine (as opposed to initializing one instance per render) and has optional caching. It also has a couple other features, like logging, NVelocity property overriding and loading templates from assembly resources.
Related
I have a requirement, at least for now, to create a subdirectory based on a username for a .NET Core website. Where is the best place to do this?
I tried adding in ApplicationUser and I am not sure how to add it correctly. What I have, which I know is completely wrong, is the following.
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Hosting;
using System.IO;
namespace BRSCRM.Models
{
// Add profile data for application users by adding properties to the ApplicationUser class
public class ApplicationUser : IdentityUser
{
private IHostingEnvironment hostingEnvironment;
public string HomeDir { get; set; }
HomeDir=HostingEnvironment.WebRootPath + UserName;
string path = this.hostingEnvironment.WebRootPath + "\\uploads\\" + UserName;
if (!Directory.Exists(path))
Directory.CreateDirectory(path);
}
I wish the documentation was better. It seems they have plenty of getting-started material out there, but when you go to try and do something that is not covered it gets pretty tough to find help.
What I am trying to do is supportfileuploading for members.
I think I am getting closer, but I get this error now:
> 'Microsoft.AspNetCore.Hosting.IHostingEnvironment'. Model bound complex types must not be abstract or value types and must have a parameterless constructor Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ComplexTypeModelBinder.CreateModel(ModelBindingContext
I cannot seem to read the IHostingEnvironment webrootpath. It is so frustrating!!
I moved my code into the Register action in file AccountController.cs...
This is what I have so far..
if (result.Succeeded)
{
await _userManager.AddToRoleAsync(user, "Member");
_logger.LogInformation("User created a new account with password.");
// Add code here to create a directory...
string webRootPath = _hostingEnvironment.WebRootPath;
string contentRootPath = _hostingEnvironment.ContentRootPath;
var userId = User.FindFirstValue(ClaimTypes.Email);
string path = webRootPath + "\\uploads\\" + userId;
if (!Directory.Exists(path))
Directory.CreateDirectory(path);
I removed the code for the environment since it didn’t work anyway. I tried to just add a directory on my local system, but I discovered that I am not getting anything in the claims field. I am not sure how to get the username, email or anything else out of it. What should I do?
The code is 1) syntactically and 2) ideologically incorrect.
The following code must be in some method, not in the model class definition
if (!Directory.Exists(path))
Directory.CreateDirectory(path);
The main idea of MVC is to separate the model definition (M), business logic (controller C), and presentation (view V). So a part of the code should be in some controller where the folder is first required (for example, AccountController) and called from (for example) [HttpPost]Register action.
private void SetUserFolder(ApplicationUser user)
{
IHostingEnvironment hostingEnvironment = /*getEnv()*/;
user.HomeDir = HostingEnvironment.WebRootPath + user.UserName;
string path = this.hostingEnvironment.WebRootPath + "\\uploads\\" + UserName;
if (!Directory.Exists(path))
Directory.CreateDirectory(path);
}
Would it meet your requirements to just check if the folder exists when the user uploads a file, then create it before saving the file if it doesn't?
As an example, if your action method (assuming MVC) is like so:
Upload files in ASP.NET Core
[HttpPost("UploadFiles")]
public async Task<IActionResult> Post(List<IFormFile> files)
{
long size = files.Sum(f => f.Length);
// Full path to file in temp location
var filePath = Path.GetTempFileName();
foreach (var formFile in files)
{
if (formFile.Length > 0)
{
using (var stream = new FileStream(filePath, FileMode.Create))
{
await formFile.CopyToAsync(stream);
}
}
}
// Process uploaded files
// Don't rely on or trust the FileName property without validation.
return Ok(new { count = files.Count, size, filePath});
}
You could simply use your own path in place of
var filePath = Path.GetTempFileName();
and check for its existence/create it if needed before saving.
public string GetRazorViewAsString(string filePath, object model = null)
{
var resultString = new StringWriter();
var context = new HttpContextWrapper(HttpContext.Current);
var routeData = new RouteData();
// Creatign the controller context
var controllerContext = new ControllerContext(new RequestContext(context, routeData), new DummyController());
// Rebdering the view and getting the html to resultString
var razor = new RazorView(controllerContext, filePath, null, false, null);
razor.Render(new ViewContext(controllerContext, razor, new ViewDataDictionary(model), new TempDataDictionary(), resultString), resultString);
// Returning the generated html
return resultString.ToString();
}
public class DummyController : Controller { }
Currently, we are using above code for generating HTML for a view.
In that, view path is a virtual path.
Now, we are planning to move the views outside of the project. So keeping virtual path is not possible now.
Is there any way of creating HTML by taking full path of the view
You can implement a VirtualPathProvider. Create a class that inherits from VirtualPathProvider, then override two methods:
FileExists
GetFile
These methods accept a virtual path argument, which you can then map to some location on disk. Your controllers will be unaware that this provider exists (they continue to use virtual paths for views). You may need to also implement a VirtualFile.
For more information, see http://www.umbraworks.net/bl0g/rebuildall/2009/11/17/ASP_NET_MVC_and_virtual_views. This blog post is sourcing views from a database, but you can adapt it to source views from anywhere.
I have partially implemented Globalization/Localization in my project. The project requires a database to be used for resource strings and I found an excellent NuGet package called WestWind.Globalization that does exactly what I needed.
This NuGet package allows you to display resource strings using several different methods. It provides an option to generate a strongly typed class that contains all of your resource strings so you can use it like:
#Html.Encode( Resources.lblResourceName )
or
object Value = this.GetLocalResourceObject("ResourceName");
or
object GlobalValue = this.GetGlobalResourceObject("Resources","ResourceKey");
and even:
dbRes.T(resourceName, resourceSet, culture)
I didn't want to specify the culture manually, so I opted for this method:
<p class="pageprompt">#AccountRequestAccount.pagePrompt</p>
For me, Westwind.Globalization is magical. It resolved a huge issue for me, but I ran into a snag that I wasn't sure how to overcome. That was, how to set the Culture/CultureUI so that the package would automatically use a specified language resource.
I created a PartialView that contains a dropdown list of languages. It is contained in the ~/Views/Shared/ folder and gets included in _Layout.cshtml. I coded the GET and POST Controller Actions which work as intended, except that I was unable to persist the Culture/CultureUI settings. I suspect that it was due to a redirect immediately following language selection (explained below)
So, I found an SO question that had an answer that seemed viable. I integrated that answer into my project. The relevant code is:
RouteConfig.cs:
routes.MapRoute("DefaultLocalized",
"{language}-{culture}/{controller}/{action}/{id}",
new
{
controller = "Home",
action = "Index",
id = "",
language = "en",
culture = "US"
});
~/Helpers/InternationalizationAttribute.cs
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Web;
using System.Web.Mvc;
namespace GPS_Web_App.Helpers
{
public class InternationalizationAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
string language =
(string)filterContext.RouteData.Values["language"] ?? "en";
string culture =
(string)filterContext.RouteData.Values["culture"] ?? "US";
Thread.CurrentThread.CurrentCulture =
CultureInfo.GetCultureInfo(string.Format("{0}-{1}",
language, culture));
Thread.CurrentThread.CurrentUICulture =
CultureInfo.GetCultureInfo(string.Format("{0}-{1}",
language, culture));
}
}
}
In my Controllers:
[Authorize]
[Internationalization]
public class AccountController : Controller
{
...
}
So far so good. This works in that I am able to go to a URL of http://example.com/en-mx/Account/Login/ and see the page being localized by Westwind.Globalization and the resource strings I've created.
The problems I have with this are:
If the user is anonymous their language preference should be controlled by cookie (if it exists) otherwise default to en-US.
If the user is authenticated their language preference should be controlled by the Language field in their profile settings. (Simple Membership using ASP.NET Identity 2.0).
There is a language selection dropdown in a global header. The user should be able to choose their language preference from the dropdown and if they do, the setting gets written to cookie (for both anonymous and authenticated users) and if the user is authenticated their Language setting in the user profile gets updated.
Not the end of the world, but it would be highly preferable that the language not be included in the URL. Some might ask, well why did I install #jao's solution? Let me explain that.
All of the code was in place for the dropdown to allow a user to make a language selection. The logic for #1, #2, and #3 above were working correctly, but wouldn't take effect and trigger Westwind.Globalization's DbResourceProvider to pass the selected language resource strings.
What I discovered through debugging was that my settings were not persisting in:
System.Threading.Thread.CurrentThread.CurrentCulture =
System.Globalization.CultureInfo.GetCultureInfo(SelectedLanguage);
System.Threading.Thread.CurrentThread.CurrentUICulture =
System.Globalization.CultureInfo.GetCultureInfo(SelectedLanguage);
Through responses provided by my question here on SO I learned that those settings would not persist/take effect if a redirect was made prior to the original View rendering. Yet a redirect back to the original View seemed sensible since the language was being changed and needed to be rendered again. I think #jao's solution overcomes the redirect problem, but it forces Globalization/Localization to be specified by the URL? Somewhat of a catch-22...
I have asked #jao to review this question and provide any hints on this. I think my question is best summed up as this:
How can I use the user's cookie/profile settings to set the Culture/CultureUI once and for all so that Westwind.Globalization can read Globalization/Localization instead of relying on the Culture being passed in the URL?
I am posting this answer as an alternate, custom way of doing localization with ASP.NET MVC5 with asynchronous controller. Perhaps you may find some gotchas in my solution especially when it comes to routing and setting cookies.
This is sort of a short tutorial I scribbled down for my heterogeneous/custom approach. So I preferred SO over WordPress. :)
Sorry for not giving the precise and discrete answer to your problem. Hopefully it will help you in some other way, and other folks as well; who are looking to do the same sort of setup.
In his blog post, Nadeem Afana described a strategy of creating a separate project Resource in the solution to implement internationalization using static resource files. In the blog sequel, he detailed on extending the same project to handle resources via Databases and XML-driven approaches. For the former one, he used ADO.NET, decoupled from Entity Framework.
We needed to implement both static and dynamic resources within the MVC project, respecting the concepts of MVC conventions.
First lets add a Resources folder in project root, with the desired language variants: ~/Resources/Resources.resx (the default resource file corresponds to en-US culture), ~/Resources/Resources.fi.resx and ~/Resources/Resources.nl.resx. Mark the resources as public, so to make them available in Views.
In ~/Views/Web.config, add the resources namespace under <namespace> element: <add namespace="YourMainNamespace.Reousrces" />. Under controllers, create a base controller class:
Here comes the cookies
namespace YourNamespace.Controllers
{
// Don't forget to inherit other controllers with this
public class BaseController : Controller
{
protected override IAsyncResult BeginExecuteCore(AsyncCallback callback, object state)
{
string cultureName = null;
// Attempt to read the culture cookie from Request
HttpCookie cultureCookie = Request.Cookies["_culture"];
if (cultureCookie != null)
cultureName = cultureCookie.Value;
else
cultureName = Request.UserLanguages != null && Request.UserLanguages.Length > 0 ?
Request.UserLanguages[0] : // obtain it from HTTP header AcceptLanguages
null;
// Validate culture name
cultureName = CultureHelper.GetImplementedCulture(cultureName); // This is safe
// Modify current thread's cultures
Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo(cultureName);
Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture;
return base.BeginExecuteCore(callback, state);
}
}
}
Next, register a global filter to in ~/Global.asax.cs to ensure that every action should use the correct culture before executing:
Here comes the cookies again!
public class SetCultureActionFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
base.OnActionExecuting(filterContext);
var response = filterContext.RequestContext.HttpContext.Response;
var culture = filterContext.RouteData.Values["culture"].ToString();
// Validate input
culture = CultureHelper.GetImplementedCulture(culture);
Thread.CurrentThread.CurrentUICulture = new CultureInfo(culture);
// Save culture in a cookie
HttpCookie cookie = filterContext.RequestContext.HttpContext.Request.Cookies["_culture"];
if (cookie != null)
cookie.Value = culture; // update cookie value
else
{
cookie = new HttpCookie("_culture");
cookie.Value = culture;
cookie.Expires = DateTime.Now.AddYears(1);
}
response.Cookies.Add(cookie);
}
}
And add GlobalFilters.Filters.Add(new SetCultureActionFilterAttribute()); in MyApplication.Application_Start() method.
In ~/App_Start/RoutesConfig.cs, change the default route to:
routes.MapRoute(
name: "Default",
url: "{culture}/{controller}/{action}/{id}",
defaults: new { culture = "en-US", controller = "Home", action = "Index", id = UrlParameter.Optional }
);
At this point, we would be able to use resources in view. For instance; #Resources.Headline.
Next, we will create a custom attribute called Translatable for model properties.
class TranslatableAttribute : Attribute
{ }
This is enough. But if you want to be able to specify scope, you can use this class to implement it.
Now add a model called Resource with three properties and a helper method:
public class Resource
{
[Key, Column(Order = 0)]
public string Culture { get; set; }
[Key, Column(Order = 1)]
public string Name { get; set; }
public string Value { get; set; }
#region Helpers
// Probably using reflection not the best approach.
public static string GetPropertyValue<T>(string id, string propertyName) where T : class
{
return GetPropertyValue<T>(id, propertyName, Thread.CurrentThread.CurrentUICulture.Name);
}
public static string GetPropertyValue<T>(string id, string propertyName, string culture) where T : class
{
Type entityType = typeof(T);
string[] segments = propertyName.Split('.');
if (segments.Length > 1)
{
entityType = Type.GetType("YourNameSpace.Models." + segments[0]);
propertyName = segments[1];
}
if (entityType == null)
return "?<invalid type>";
var propertyInfo = entityType.GetProperty(propertyName);
var translateableAttribute = propertyInfo.GetCustomAttributes(typeof(TranslatableAttribute), true)
.FirstOrDefault();
/*var requiredAttribute = propertyInfo.GetCustomAttributes(typeof(RequiredAttribute), true)
.FirstOrDefault();*/
if (translateableAttribute == null)
return "?<this field has no translatable attribute>";
var dbCtx = new YourNamespaceDbContext();
var className = entityType.Name;
Resource resource = dbCtx.Resources.Where(r =>
(r.Culture == culture) &&
r.Name == className + id + propertyName).FirstOrDefault();
if (resource != null)
return resource.Value;
//return requiredAttribute == null ? string.Empty : "?<translation not found>";
return string.Empty;
}
#endregion
}
This helper method will help you retrieve the translated content. For instance in view, you can say:
var name = Resource.GetPropertyValue<Product>(item.Id.ToString(), "Name");
Note that, at any point, the data in the translatable field column is unreliable; it will always hold the last updated value. On creating the record, we will mirror all the translatable properties' values in Resource model for all supported cultures.
We are using asynchronous controllers, so for insertion, modification and deletion we will be overriding SaveChangesAsync() in our DbContext class:
public override Task<int> SaveChangesAsync()
{
ObjectContext ctx = ((IObjectContextAdapter)this).ObjectContext;
List<ObjectStateEntry> objectDeletedStateEntryList =
ctx.ObjectStateManager.GetObjectStateEntries(EntityState.Deleted)
.ToList();
List<ObjectStateEntry> objectCreateOrModifiedStateEntryList =
ctx.ObjectStateManager.GetObjectStateEntries(EntityState.Added
| EntityState.Modified)
.ToList();
// First handle the delition case,
// before making changes to entry state
bool changed = UpdateResources(objectDeletedStateEntryList);
// Now save the changes
int result = base.SaveChangesAsync().Result;
// Finally handle the remaining cases
changed |= UpdateResources(objectCreateOrModifiedStateEntryList);
if (changed)
return base.SaveChangesAsync();
return Task.FromResult<int>(result);
}
private bool UpdateResources(List<ObjectStateEntry> objectStateEntryList)
{
bool changed = false;
foreach (ObjectStateEntry entry in objectStateEntryList)
{
var typeName = entry.EntitySet.ElementType.Name;
if (entry.IsRelationship || typeName == "Resource")
return false;
var type = Type.GetType("YourNamespace.Models." + typeName);
if (type == null) // When seeds run (db created for the first-time), sometimes types might not be create
return false;
if (entry.State == EntityState.Deleted)
{
changed |= DeleteResources(type, typeName, entry);
continue;
}
foreach (var propertyInfo in type.GetProperties())
{
var attribute = propertyInfo.GetCustomAttributes(typeof(TranslatableAttribute), true).FirstOrDefault();
if (attribute == null)
continue;
CurrentValueRecord current = entry.CurrentValues;
object idField = current.GetValue(current.GetOrdinal("Id"));
if (idField == null)
continue;
var id = idField.ToString();
var propertyName = propertyInfo.Name;
string newValue = current.GetValue(current.GetOrdinal(propertyName)).ToString();
var name = typeName + id + propertyName;
Resource existingResource = this.Resources.Find(Thread.CurrentThread.CurrentUICulture.Name, name);
if (existingResource == null)
{
foreach (var culture in CultureHelper.Cultures)
{
this.Resources.Add(new Resource
{
Culture = culture,
Name = name,
Value = newValue
});
changed |= true;
}
}
else
{
existingResource.Value = newValue;
changed |= true;
}
}
}
return changed;
}
private bool DeleteResources(Type type, string typeName, ObjectStateEntry entry)
{
bool changed = false;
var firstKey = entry.EntityKey.EntityKeyValues.Where(k => k.Key.Equals("Id", StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault();
if (firstKey == null)
return false;
var id = firstKey.Value.ToString();
foreach (var propertyInfo in type.GetProperties())
{
var name = typeName + id + propertyInfo.Name;
foreach (var culture in CultureHelper.Cultures)
{
Resource existingResource = this.Resources.Find(culture, name);
if (existingResource == null)
continue;
this.Resources.Remove(existingResource);
changed |= true;
}
}
return changed;
}
This will take care of update and delete.
I'm writing specflow tests using Watin, for an Asp.Net MVC application which uses T4MVC.
I find myself using "magic string" urls in the tests, which I don't like.
[Given(#"I am on the sign up page")]
public void GivenIAmOnTheSignUpPage()
{
string rootUrl = ConfigurationManager.AppSettings["RootUrl"];
string fullUrl = string.Format("{0}/Authentication/Signup",rootUrl);
WebBrowser.Current.GoTo(fullUrl);
}
I would much rather use my T4MVC Action Results like I do in the MVC App, something like this...
[Given(#"I am on the sign up page")]
public void GivenIAmOnTheSignUpPage()
{
WebBrowser.Current.GoTo(MVC.Authentication.SignUp().ToAbsoluteUrl());
}
My ToAbsoluteUrl Extension Method
public static class RouteHelper
{
private static UrlHelper _urlHelper;
private static string _rootUrl;
public static string ToAbsoluteUrl(this ActionResult result)
{
EnsureUrlHelperInitialized();
var relativeUrl = _urlHelper.Action(result);
return string.Format("{0}/{1}", _rootUrl, relativeUrl);
}
private static void EnsureUrlHelperInitialized()
{
if (_urlHelper==null)
{
_rootUrl = ConfigurationManager.AppSettings["RootUrl"];
var request = new HttpRequest("/", _rootUrl, "");
var response = new HttpResponse(new StringWriter());
var context = new HttpContext(request,response);
HttpContext.Current = context;
var httpContextBase = new HttpContextWrapper(context);
RouteTable.Routes.Clear();
MvcApplication.RegisterRoutes(RouteTable.Routes);
var requestContext = new RequestContext(httpContextBase, RouteTable.Routes.GetRouteData(httpContextBase));
_urlHelper = new UrlHelper(requestContext, RouteTable.Routes);
}
}
}
What is the correct way to initialize the RequestContext and RouteCollection so that I can generate my test URLs?
Currently I receive a NullReferenceException on the line var requestContext = new RequestContext(httpContextBase, RouteTable.Routes.GetRouteData(httpContextBase));. Is that the right way to new up a requestContext?
Or if there is a better way to take an ActionResult (from T4MVC) and resolve it to an absolute url, outside of a web app, that's really what I'm looking for.
public static class RouteHelper
{
private static UrlHelper _urlHelper;
private static string _rootUrl;
static RouteHelper()
{
var routes = new RouteCollection();
MvcApplication.RegisterRoutes(routes);
var req = new HttpRequest(string.Empty, "http://www.site.com", null);
var res = new HttpResponse(null);
var ctx = new HttpContext(req, res); // do not use HttpContext.Current
var requestContext = new RequestContext(new HttpContextWrapper(ctx),
new RouteData());
_urlHelper = new UrlHelper(requestContext, routes);
_rootUrl = ConfigurationManager.AppSettings["RootUrl"];
}
public static string ToAbsoluteUrl(this ActionResult result)
{
return string.Format("{0}{1}", _rootUrl, _urlHelper.Action(result));
}
}
The static constructor sets up your private fields. I chose to use a new RouteCollection, instead of using the static RouteTable.Routes property, but you might be able to.
I don't think the constructors for the HttpRequest and HttpResponse matter. I just passed in some strings to get them to construct without throwing an exception. Use those to construct a brand new HttpContext (don't use HttpContext.Current when running from xUnit). You can then put it into an HttpContextWrapper to get your HttpContextBase reference.
Construct a new RequestContext, passing in your base wrapper and a new RouteData instance. Use that, along with your previous RouteCollection to construct the UrlHelper. Note that its Action method will return strings prepended with "/", so you should leave that out of our RootUrl appSetting (so use something like value="https://develop.site.com" without the trailing slash).
Note this will not work for routes defined in MVC areas. For that, you need to register the areas in addition to calling RegisterRoutes in global asax.
What is the best way to return XML from a controller's action in ASP.NET MVC? There is a nice way to return JSON, but not for XML. Do I really need to route the XML through a View, or should I do the not-best-practice way of Response.Write-ing it?
return this.Content(xmlString, "text/xml");
Use MVCContrib's XmlResult Action.
For reference here is their code:
public class XmlResult : ActionResult
{
private object objectToSerialize;
/// <summary>
/// Initializes a new instance of the <see cref="XmlResult"/> class.
/// </summary>
/// <param name="objectToSerialize">The object to serialize to XML.</param>
public XmlResult(object objectToSerialize)
{
this.objectToSerialize = objectToSerialize;
}
/// <summary>
/// Gets the object to be serialized to XML.
/// </summary>
public object ObjectToSerialize
{
get { return this.objectToSerialize; }
}
/// <summary>
/// Serialises the object that was passed into the constructor to XML and writes the corresponding XML to the result stream.
/// </summary>
/// <param name="context">The controller context for the current request.</param>
public override void ExecuteResult(ControllerContext context)
{
if (this.objectToSerialize != null)
{
context.HttpContext.Response.Clear();
var xs = new System.Xml.Serialization.XmlSerializer(this.objectToSerialize.GetType());
context.HttpContext.Response.ContentType = "text/xml";
xs.Serialize(context.HttpContext.Response.Output, this.objectToSerialize);
}
}
}
If you're building the XML using the excellent Linq-to-XML framework, then this approach will be helpful.
I create an XDocument in the action method.
public ActionResult MyXmlAction()
{
// Create your own XDocument according to your requirements
var xml = new XDocument(
new XElement("root",
new XAttribute("version", "2.0"),
new XElement("child", "Hello World!")));
return new XmlActionResult(xml);
}
This reusable, custom ActionResult serialises the XML for you.
public sealed class XmlActionResult : ActionResult
{
private readonly XDocument _document;
public Formatting Formatting { get; set; }
public string MimeType { get; set; }
public XmlActionResult(XDocument document)
{
if (document == null)
throw new ArgumentNullException("document");
_document = document;
// Default values
MimeType = "text/xml";
Formatting = Formatting.None;
}
public override void ExecuteResult(ControllerContext context)
{
context.HttpContext.Response.Clear();
context.HttpContext.Response.ContentType = MimeType;
using (var writer = new XmlTextWriter(context.HttpContext.Response.OutputStream, Encoding.UTF8) { Formatting = Formatting })
_document.WriteTo(writer);
}
}
You can specify a MIME type (such as application/rss+xml) and whether the output should be indented if you need to. Both properties have sensible defaults.
If you need an encoding other than UTF8, then it's simple to add a property for that too.
If you are only interested to return xml through a request, and you have your xml "chunk", you can just do (as an action in your controller):
public string Xml()
{
Response.ContentType = "text/xml";
return yourXmlChunk;
}
There is a XmlResult (and much more) in MVC Contrib. Take a look at http://www.codeplex.com/MVCContrib
I've had to do this recently for a Sitecore project which uses a method to create an XmlDocument from a Sitecore Item and its children and returns it from the controller ActionResult as a File. My solution:
public virtual ActionResult ReturnXml()
{
return File(Encoding.UTF8.GetBytes(GenerateXmlFeed().OuterXml), "text/xml");
}
use one of these methods
public ContentResult GetXml()
{
string xmlString = "your xml data";
return Content(xmlString, "text/xml");
}
or
public string GetXml()
{
string xmlString = "your xml data";
Response.ContentType = "text/xml";
return xmlString;
}
Finally manage to get this work and thought I would document how here in the hopes of saving others the pain.
Environment
VS2012
SQL Server 2008R2
.NET 4.5
ASP.NET MVC4 (Razor)
Windows 7
Supported Web Browsers
FireFox 23
IE 10
Chrome 29
Opera 16
Safari 5.1.7 (last one for Windows?)
My task was on a ui button click, call a method on my Controller (with some params) and then have it return an MS-Excel XML via an xslt transform. The returned MS-Excel XML would then cause the browser to popup the Open/Save dialog. This had to work in all the browsers (listed above).
At first I tried with Ajax and to create a dynamic Anchor with the "download" attribute for the filename,
but that only worked for about 3 of the 5 browsers(FF, Chrome, Opera) and not for IE or Safari.
And there were issues with trying to programmatically fire the Click event of the anchor to cause the actual "download".
What I ended up doing was using an "invisible" IFRAME and it worked for all 5 browsers!
So here is what I came up with:
[please note that I am by no means an html/javascript guru and have only included the relevant code]
HTML (snippet of relevant bits)
<div id="docxOutput">
<iframe id="ifOffice" name="ifOffice" width="0" height="0"
hidden="hidden" seamless='seamless' frameBorder="0" scrolling="no"></iframe></div>
JAVASCRIPT
//url to call in the controller to get MS-Excel xml
var _lnkToControllerExcel = '#Url.Action("ExportToExcel", "Home")';
$("#btExportToExcel").on("click", function (event) {
event.preventDefault();
$("#ProgressDialog").show();//like an ajax loader gif
//grab the basket as xml
var keys = GetMyKeys();//returns delimited list of keys (for selected items from UI)
//potential problem - the querystring might be too long??
//2K in IE8
//4096 characters in ASP.Net
//parameter key names must match signature of Controller method
var qsParams = [
'keys=' + keys,
'locale=' + '#locale'
].join('&');
//The element with id="ifOffice"
var officeFrame = $("#ifOffice")[0];
//construct the url for the iframe
var srcUrl = _lnkToControllerExcel + '?' + qsParams;
try {
if (officeFrame != null) {
//Controller method can take up to 4 seconds to return
officeFrame.setAttribute("src", srcUrl);
}
else {
alert('ExportToExcel - failed to get reference to the office iframe!');
}
} catch (ex) {
var errMsg = "ExportToExcel Button Click Handler Error: ";
HandleException(ex, errMsg);
}
finally {
//Need a small 3 second ( delay for the generated MS-Excel XML to come down from server)
setTimeout(function () {
//after the timeout then hide the loader graphic
$("#ProgressDialog").hide();
}, 3000);
//clean up
officeFrame = null;
srcUrl = null;
qsParams = null;
keys = null;
}
});
C# SERVER-SIDE (code snippet)
#Drew created a custom ActionResult called XmlActionResult which I modified for my purpose.
Return XML from a controller's action in as an ActionResult?
My Controller method (returns ActionResult)
passes the keys parameter to a SQL Server stored proc that generates an XML
that XML is then transformed via xslt into an MS-Excel xml (XmlDocument)
creates instance of the modified XmlActionResult and returns it
XmlActionResult result = new XmlActionResult(excelXML, "application/vnd.ms-excel");
string version = DateTime.Now.ToString("dd_MMM_yyyy_hhmmsstt");
string fileMask = "LabelExport_{0}.xml";
result.DownloadFilename = string.Format(fileMask, version);
return result;
The main modification to the XmlActionResult class that #Drew created.
public override void ExecuteResult(ControllerContext context)
{
string lastModDate = DateTime.Now.ToString("R");
//Content-Disposition: attachment; filename="<file name.xml>"
// must set the Content-Disposition so that the web browser will pop the open/save dialog
string disposition = "attachment; " +
"filename=\"" + this.DownloadFilename + "\"; ";
context.HttpContext.Response.Clear();
context.HttpContext.Response.ClearContent();
context.HttpContext.Response.ClearHeaders();
context.HttpContext.Response.Cookies.Clear();
context.HttpContext.Response.Cache.SetCacheability(System.Web.HttpCacheability.NoCache);// Stop Caching in IE
context.HttpContext.Response.Cache.SetNoStore();// Stop Caching in Firefox
context.HttpContext.Response.Cache.SetMaxAge(TimeSpan.Zero);
context.HttpContext.Response.CacheControl = "private";
context.HttpContext.Response.Cache.SetLastModified(DateTime.Now.ToUniversalTime());
context.HttpContext.Response.ContentType = this.MimeType;
context.HttpContext.Response.Charset = System.Text.UTF8Encoding.UTF8.WebName;
//context.HttpContext.Response.Headers.Add("name", "value");
context.HttpContext.Response.Headers.Add("Last-Modified", lastModDate);
context.HttpContext.Response.Headers.Add("Pragma", "no-cache"); // HTTP 1.0.
context.HttpContext.Response.Headers.Add("Expires", "0"); // Proxies.
context.HttpContext.Response.AppendHeader("Content-Disposition", disposition);
using (var writer = new XmlTextWriter(context.HttpContext.Response.OutputStream, this.Encoding)
{ Formatting = this.Formatting })
this.Document.WriteTo(writer);
}
That was basically it.
Hope it helps others.
A simple option that will let you use streams and all that is return File(stream, "text/xml");.
Here is a simple way of doing it:
var xml = new XDocument(
new XElement("root",
new XAttribute("version", "2.0"),
new XElement("child", "Hello World!")));
MemoryStream ms = new MemoryStream();
xml.Save(ms);
return File(new MemoryStream(ms.ToArray()), "text/xml", "HelloWorld.xml");
A small variation of the answer from Drew Noakes that use the method Save() of XDocument.
public sealed class XmlActionResult : ActionResult
{
private readonly XDocument _document;
public string MimeType { get; set; }
public XmlActionResult(XDocument document)
{
if (document == null)
throw new ArgumentNullException("document");
_document = document;
// Default values
MimeType = "text/xml";
}
public override void ExecuteResult(ControllerContext context)
{
context.HttpContext.Response.Clear();
context.HttpContext.Response.ContentType = MimeType;
_document.Save(context.HttpContext.Response.OutputStream)
}
}