I want to call the #Html.ActionLink method inside a c# function to return a string with a link on it.
Something like this:
string a = "Email is locked, click " + #Html.ActionLink("here to unlock.", "unlock") ;
Assuming that you want to accomplish this in your controller, there are several hoops to jump through. You must instantiate a ViewDataDictionary and a TempDataDictionary. Then you need to take the ControllerContext and create an IView. Finally, you are ready to create your HtmlHelper using all of these elements (plus your RouteCollection).
Once you have done all of this, you can use LinkExtensions.ActionLink to create your custom link. In your view, you will need to use #Html.Raw() to display your links, to prevent them from being HTML encoded. Here is the necessary code:
var vdd = new ViewDataDictionary();
var tdd = new TempDataDictionary();
var controllerContext = this.ControllerContext;
var view = new RazorView(controllerContext, "/", "/", false, null);
var html = new HtmlHelper(new ViewContext(controllerContext, view, vdd, tdd, new StringWriter()),
new ViewDataContainer(vdd), RouteTable.Routes);
var a = "Email is locked, click " + LinkExtensions.ActionLink(html, "here to unlock.", "unlock", "controller").ToString();
Having shown all of this, I will caution you that it is a much better idea to do this in your view. Add the error and other information to your ViewModel, then code your view to create the link. If this is needed across multiple views, create an HtmlHelper to do the link creation.
UPDATE
To address one.beat.consumer, my initial answer was an example of what is possible. If the developer needs to reuse this technique, the complexity can be hidden in a static helper, like so:
public static class ControllerHtml
{
// this class from internal TemplateHelpers class in System.Web.Mvc namespace
private class ViewDataContainer : IViewDataContainer
{
public ViewDataContainer(ViewDataDictionary viewData)
{
ViewData = viewData;
}
public ViewDataDictionary ViewData { get; set; }
}
private static HtmlHelper htmlHelper;
public static HtmlHelper Html(Controller controller)
{
if (htmlHelper == null)
{
var vdd = new ViewDataDictionary();
var tdd = new TempDataDictionary();
var controllerContext = controller.ControllerContext;
var view = new RazorView(controllerContext, "/", "/", false, null);
htmlHelper = new HtmlHelper(new ViewContext(controllerContext, view, vdd, tdd, new StringWriter()),
new ViewDataContainer(vdd), RouteTable.Routes);
}
return htmlHelper;
}
public static HtmlHelper Html(Controller controller, object model)
{
if (htmlHelper == null || htmlHelper.ViewData.Model == null || !htmlHelper.ViewData.Model.Equals(model))
{
var vdd = new ViewDataDictionary();
vdd.Model = model;
var tdd = new TempDataDictionary();
var controllerContext = controller.ControllerContext;
var view = new RazorView(controllerContext, "/", "/", false, null);
htmlHelper = new HtmlHelper(new ViewContext(controllerContext, view, vdd, tdd, new StringWriter()),
new ViewDataContainer(vdd), RouteTable.Routes);
}
return htmlHelper;
}
}
Then, in a controller, it is used like so:
var a = "Email is locked, click " +
ControllerHtml.Html(this).ActionLink("here to unlock.", "unlock", "controller").ToString();
or like so:
var model = new MyModel();
var text = ControllerHtml.Html(this, model).EditorForModel();
While it is easier to use Url.Action, this now extends into a powerful tool to generate any mark-up within a controller using all of the HtmlHelpers (with full Intellisense).
Possibilities of use include generating mark-up using models and Editor templates for emails, pdf generation, on-line document delivery, etc.
You could create an HtmlHelper extension method:
public static string GetUnlockText(this HtmlHelper helper)
{
string a = "Email is locked, click " + helper.ActionLink("here to unlock.", "unlock");
return a;
}
or if you mean to generate this link outside of the scope of an aspx page you'll need to create a reference to an HtmlHelper and then generate. I do this in a UrlUtility static class (I know, people hate static classes and the word Utility, but try to focus). Overload as necessary:
public static string ActionLink(string linkText, string actionName, string controllerName)
{
var httpContext = new HttpContextWrapper(System.Web.HttpContext.Current);
var requestContext = new RequestContext(httpContext, new RouteData());
var urlHelper = new UrlHelper(requestContext);
return urlHelper.ActionLink(linkText, actionName, controllerName, null);
}
Then you can write the following from wherever your heart desires:
string a = "Email is locked, click " + UrlUtility.ActionLink("here to unlock.", "unlock", "controller");
There's a couple things bad about these other answers...
Shark's answer requires you to bring the LinkExtensions namespace into C#, which is not wrong, but undesirable to me.
Hunter's idea of making a helper is a better one, but still writing a helper function for a single URL is cumbersome. You could write a helper to help you build strings that accepted parameters, or you could simply do it the old fashion way:
var link = "Email is... click, to unlock.";
#counsellorben,
i see no reason for the complexity; the user wants only to render an Action's routing into a hard string containing an anchor tag. Moreover, ActionLink() in a hard-written concatenated string buys one nothing, and forces the developer to use LinkExtensions whidh are intended for Views.
If the user is diehard about using ActionLink() or does not need (for some reason) to calculate this string in the constructor, doing so in a view is much better.
I still stand by and recommend the answers tvanfosson and I provided.
Your best bet is to construct the link manually using the UrlHelper available in the controller. Having said that, I'm suspicious that there is probably a better way to handle this in a view or partial view, shared or otherwise.
string a = "Email is locked, click <a href=\""
+ Url.Action( "unlock" )
+ "\">here to unlock.</a>";
Maybe try this:
string a = "Email is locked, click " + System.Web.Mvc.Html.LinkExtensions.ActionLink("here to unlock.", "unlock");
Related
When using TagHelpers with MVC Core (2.1) it is possible to define asp-all-route-data and additional asp-route-x. But if x is alreay contained inside the object passed to all-route-data, an exception is thrown:
An element with the key 'x' already exists in the RouteValueDictionary
Is it possible to change this behavior (extend TagHelper, custom TagHelper, etc) to just update the value in this case instead of trying to create a new entry.
So this:
if(RouteValueDictionary.HasKey(x))
RouteValueDictionary[x] = value;
instead of (probably something like) this:
RouteValueDictionary.Add(x, value);
(Basically the if in my code isn't even necessary in an assignment like this)
Edit (maybe to explain the use case): There is an object with all the query parameters for a form, but there are also links that should basically send the same data and change a value. Submitting the form would only work if using javascript with an onclick for every link, setting the hidden input and submitting the form. But I would prefer not using JS for this as features like "middle click to new tab" will be lost.
The Answer from Tseng is very useful, however if it's necessary to use an Object for the Values and not the context it might not work that well.
In the end we created a TagHelper that only creates the query parameters:
[HtmlTargetElement("a", Attributes = RouteValuesDictionaryName)]
[HtmlTargetElement("a", Attributes = RouteValuesPrefix + "*")]
public class SearchLinkTagHelper : TagHelper
{
private const string RouteValuesDictionaryName = "my-all-route-data";
private const string RouteValuesPrefix = "my-route-";
private const string Href = "href";
private IDictionary<string, string> _routeValues;
public override int Order => 1;
[HtmlAttributeName(RouteValuesDictionaryName, DictionaryAttributePrefix = RouteValuesPrefix)]
public IDictionary<string, string> RouteValues
{
get => _routeValues ?? (_routeValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase));
set => _routeValues = value;
}
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.Attributes.TryGetAttribute(Href, out var href);
var fixedRouteValues = new Dictionary<string, string>();
foreach (var (newKey, value) in _routeValues.Where(r=>!string.IsNullOrWhiteSpace(r.Value)))
{
var key = fixedRouteValues.Keys.FirstOrDefault(k => string.Equals(k, newKey, StringComparison.InvariantCultureIgnoreCase)) ?? newKey;
fixedRouteValues[key] = value;
}
var query = string.Join("&", fixedRouteValues.Select(kvp => $"{kvp.Key}={kvp.Value}"));
var hrefValue = href.Value;
output.Attributes.Remove(href);
href = new TagHelperAttribute(Href, hrefValue + "?" +query);
output.Attributes.Add(href);
}
}
I'm trying to create a ResetPassword Page and I need to create something like that!
myApi.azure.com/ResetPassword?hash=YYYYYYYYYYYYYY
I already know how to create a link to another controller, but that way it would trigger the Action just with the click, and what I need is pass the hash as parameter inside of that URL and them, call a controller!
var link = new Uri(Url.Link("ValidationEmailUser", new { Code = emailToken }));
Something like this:
public IHttpActionResult RedirectAction()
{
var urlFormat = string.Format("https://www3.olx.com.br/account/forgotten_password/?hash={0}", emailToken);
var location = new Uri(urlFormat);
return this.Redirect(location);
}
I'm building a project using the new ASP.Net 5/MVC 6 libraries and I've come to a part where I want to dynamically render a razor page to a string.
This is for a dynamic widget-system so the idea is to do this in an ActionFilter after generating the model but before rendering the main view.
The setup for it is quite large but it's mostly requiring services and putting pieces together (if someone knows a simpler way then please tell!).
public override void OnActionExecuted(ActionExecutedContext context) {
var pageFactory = (IRazorPageFactory)context.HttpContext.ApplicationServices.GetService(typeof(IRazorPageFactory));
var viewEngine = (IRazorViewEngine)context.HttpContext.ApplicationServices.GetService(typeof(IRazorViewEngine));
var viewFactory = (IRazorViewFactory)context.HttpContext.ApplicationServices.GetService(typeof(IRazorViewFactory));
var modelMetadataProvider = (IModelMetadataProvider)context.HttpContext.ApplicationServices.GetService(typeof(IModelMetadataProvider));
var page = pageFactory.CreateInstance($"~/Views/Widgets/{widgetInfo.Type}.cshtml");
var view = viewFactory.GetView(viewEngine, page, false);
var sb = new StringBuilder();
var sw = new StringWriter(sb);
page.ViewContext = new ViewContext(new ActionContext(context.HttpContext, context.RouteData, context.ActionDescriptor), view, new ViewDataDictionary(modelMetadataProvider, new ModelStateDictionary()), sw);
After setting all this up I try to set the Model-property on my ViewData and I can see that I've set the correct object and that it is of the correct type for the page.
page.ViewContext.ViewData.Model = widgetInfo.Setting;
I then execute the page which works great when I'm not using any models in my template.
page.ExecuteAsync().GetAwaiter().GetResult();
sw.Flush();
var renderedWidget = sb.ToString();
I can see that my template is loaded (and even put breakpoints within it) but Model is always null. Currently I use this simple template.
#model DataObjects.CodeWidgetModel
#Html.Raw(Model.Code)
I've been scouring the MVC6-sources over at GitHub to see what I could have missed but I can't find anything useful.
I'm running ASP.Net MVC 6.0.0-beta3-12628 and KRE 1.0.0-beta3-11014 but will most probably upgrade to beta4 as soon as it's available.
Anyone have any idea why setting Model on ViewData isn't enough or what else I need to do to get this working?
After some more research (quite a lot of just trial and error to be honest) it seems that RazorPage.ExecuteAsync() wasn't the right method for this. Calling RazorView.RenderAsync() instead with a correctly typed ViewDataDictionary<> did the trick!
Below is the final code.
public override void OnActionExecuted(ActionExecutedContext context) {
var pageFactory = (IRazorPageFactory)context.HttpContext.ApplicationServices.GetService(typeof(IRazorPageFactory));
var viewEngine = (IRazorViewEngine)context.HttpContext.ApplicationServices.GetService(typeof(IRazorViewEngine));
var viewFactory = (IRazorViewFactory)context.HttpContext.ApplicationServices.GetService(typeof(IRazorViewFactory));
var sb = new StringBuilder();
var sw = new StringWriter(sb);
var page = pageFactory.CreateInstance($"~/Areas/Widgets/DefaultViews/Widgets/{widgetInfo.Type}.cshtml");
var view = (RazorView)viewFactory.GetView(viewEngine, page, true);
var vddType = typeof(ViewDataDictionary<>);
var vddGeneric = vddType.MakeGenericType(widgetInfo.Model.GetType());
dynamic viewDataDictionary = Activator.CreateInstance(vddGeneric, new EmptyModelMetadataProvider(), new ModelStateDictionary());
var actionContext = new ActionContext(context.HttpContext, context.RouteData, context.ActionDescriptor);
var viewContext = new ViewContext(actionContext, view, viewDataDictionary, sw);
viewContext.ViewData.Model = widgetInfo.Model;
view.RenderAsync(viewContext).GetAwaiter().GetResult();
sw.Flush();
var renderedWidget = sb.ToString();
}
I'm not particularly happy with the dynamic for ViewDataDictionary but it works. There might also be other ways to do this that are easier, but it's good enough for now.
Tested on ASP.Net MVC 6.0.0-beta3-12628 and KRE 1.0.0-beta3-11014.
You are diving pretty deep into the rendering pipeline for something so simple. Try creating your model with a widgets collection. In your main view do something like:
foreach(var widget in Model.Widgets) { Html.DisplayFor(widget) }
Then create a view in /views/displaytemplates/widget.cshtml that then breaks the widget out into the correct template by casting it to the more specific type of widget and then calling Html.DisplayFor with it.
** PSEUDO code **
#model Widget
if (Widget is Widget1Type) Html.DisplayFor((Widget1Type)Widget)
if (Widget is Widget2Type) Html.DisplayFor((Widget2Type)Widget)
if (Widget is Widget3Type) Html.DisplayFor((Widget3Type)Widget)
if (Widget is Widget4Type) Html.DisplayFor((Widget4Type)Widget)
Then create your views for those widgets
/views/displaytemplates/widget1type:
#model Widget1Type
...Stuff here...
I've been bashing my head for a quite some time now and I'm pretty sure I'm missing something very obvious. I want to create a route link, that can dynamically set css class to "selected", if the current controller action matches it. It's easy, however, I'm having troubles modifying existing htmlAttributes that I need to pass in.
public static MvcHtmlString RouteLinkSelectable(this HtmlHelper html, string linkText, string routeName, object routeValues, object htmlAttributes, string controller = null, string action = null)
{
// omitting code for determining if the class should be set, because it
// doesn't modify the behavior. It does that same thing with the following code
var myAttributes = new Dictionary<string, object>
{
{ "data-myattribute1", "value1" },
{ "data-myattribute2", "value2" }
};
var attributes = new RouteValueDictionary(htmlAttributes);
// now merge them with the user attributes
foreach (var item in attributes)
{
// remove this test if you want to overwrite existing keys
if (!myAttributes.ContainsKey(item.Key))
{
myAttributes[item.Key] = item.Value;
}
}
return html.RouteLink(linkText, routeName, routeValues, myAttributes);
}
This is the code (well one of the variations I've been trying) that was suggested by Darin Dimitrov in this answer https://stackoverflow.com/a/12729240/1289283
That should work, right? Well, not exactly..
When I call it from my layout like this:
#Html.RouteLinkSelectable("profil", "Default", null, new { id = "lnkProfile" }, action: "Index")
It produces this output:
<a Comparer="System.Collections.Generic.GenericEqualityComparer`1[System.String]" Count="3" Keys="System.Collections.Generic.Dictionary`2+KeyCollection[System.String,System.Object]" Values="System.Collections.Generic.Dictionary`2+ValueCollection[System.String,System.Object]" href="/">profil</a>
If I modify the code to use classical syntax (...., new { id = "lnkProfile" }), it works good. If I create a new class with properties, it works good. If I use expando object, it doesn't attach any html properties... And if try to use a dictionary, the result is shown above... Please, can anyone explain it to me, why does it behave like this and how can I solve that?
Btw, of course I could create a link from scratch, but why reinvent the wheel when I simply need just to add one html attribute dynamically?
The problem is that you are targeting the wrong overload of RouteLink, change the return statement with the following
return html.RouteLink(linkText, routeName, new RouteValueDictionary(routeValues), myAttributes);
First, some context:
Language - C#
Platform - .Net Framework 4.5
Project type - ASP.Net MVC 4
I am trying to determine which View in an MVC project is handling an explicit call to the following method. The MSDN docs for the method are here: http://msdn.microsoft.com/EN-US/library/dd492930.aspx
protected internal ViewResult View(
Object model
)
The original Author is using a View to generate a PDF file with a third-party library. I need to modify the view to include additional information.
The problem: I'm having trouble finding which View to modify. There are hundreds of them, and (IMHO) they are poorly named and organized. The basic process for generating a PDF looks like this. I'm getting confused in between steps 3 and 4.
An Entity's ID is passed to an ActionResult
The Entity is retrieved from the backing store
The model is passed to the Controller.View method mentioned above:
var viewModel = View(model);
var xmlText = RenderActionResultToString(viewModel);
The resulting ViewResult is used with an instance of ControllerContext to generate HTML as if being requested by a browser.
The resulting HTML is passed to the third-party tool and converted to a PDF.
I understand everything else very clearly. What I don't understand is how the call to View(model) determines which View file to use when returning the ViewResult. Any help greatly appreciated!
I'm including the code below, in case it helps anybody determine the answer.
The ActionResult:
public ActionResult ProposalPDF(String id, String location, bool hidePrices = false)
{
var proposal = _adc.Proposal.GetByKey(int.Parse(id));
var opportunity = _adc.Opportunity.GetByKey(proposal.FkOpportunityId.Value);
ViewData["AccountId"] = opportunity.FkAccountId;
ViewData["AccountType"] = opportunity.FkAccount.FkAccountTypeId;
ViewData["Location"] = location;
ViewData["HidePrices"] = hidePrices;
return ViewPdf(proposal);
}
The ViewPDF method:
protected ActionResult ViewPdf(object model)
{
// Create the iTextSharp document.
var document = new Document(PageSize.LETTER);
// Set the document to write to memory.
var memoryStream = new MemoryStream();
var pdfWriter = PdfWriter.GetInstance(document, memoryStream);
pdfWriter.CloseStream = false;
document.Open();
// Render the view xml to a string, then parse that string into an XML dom.
var viewModel = View(model);
var xmlText = RenderActionResultToString(viewModel);
var htmlPipelineContext = new HtmlPipelineContext();
htmlPipelineContext.SetTagFactory(Tags.GetHtmlTagProcessorFactory());
//CSS stuff
var cssResolver = XMLWorkerHelper.GetInstance().GetDefaultCssResolver(false);
var cssResolverPipeline = new CssResolverPipeline(cssResolver, new HtmlPipeline(htmlPipelineContext, new PdfWriterPipeline(document, pdfWriter)));
var xmlWorker = new XMLWorker(cssResolverPipeline, true);
var xmlParser = new XMLParser(xmlWorker);
xmlParser.Parse(new StringReader(xmlText));
// Close and get the resulted binary data.
document.Close();
var buffer = new byte[memoryStream.Position];
memoryStream.Position = 0;
memoryStream.Read(buffer, 0, buffer.Length);
// Send the binary data to the browser.
return new BinaryContentResult(buffer, "application/pdf");
}
The RenderActionResultToString helper method:
protected string RenderActionResultToString(ActionResult result)
{
// Create memory writer.
var sb = new StringBuilder();
var memWriter = new StringWriter(sb);
// Create fake http context to render the view.
var fakeResponse = new HttpResponse(memWriter);
var fakeContext = new HttpContext(System.Web.HttpContext.Current.Request, fakeResponse);
var fakeControllerContext = new ControllerContext(new HttpContextWrapper(fakeContext), this.ControllerContext.RouteData, this.ControllerContext.Controller);
var oldContext = System.Web.HttpContext.Current;
System.Web.HttpContext.Current = fakeContext;
// Render the view.
result.ExecuteResult(fakeControllerContext);
// Restore data.
System.Web.HttpContext.Current = oldContext;
// Flush memory and return output.
memWriter.Flush();
return sb.ToString();
}
I'm not exactly sure what you're asking, but, when you call View(model) the view that is chosen is based upon conventions.
Here is an example:
public class HerbController : Controller {
public ActionResult Cilantro(SomeType model) {
return View(model)
}
}
That will look for a view file called Cilantro.cshtml in a folder called Herb (Views/Herb/Cilantro.cshtml). The framework will also look in the Shared directory as well in case it is a view that is meant to be shared across multiple results.
However, you may also want to look at the Global.asax file to see if there are any custom view paths being setup for the view engine. The example I gave above is based upon the default conventions of ASP.NET MVC. You can override them to meet your needs better if needed.
The convention for views is that they are in a folder named after the controller (without "Controller") and the .cshtml file inside that folder is named after the calling action. In your case, that should be:
~/Views/[Controller]/ProposalPdf.cshtml
The logic to determine which view template will be used is in the ViewResult that is returned from the call
var viewModel = View(model);
And how the view is selected is determined by the configured ViewEngine(s), but it will use the current Area, Controller and Action route values to determine what view should be served.
What the route values are for the ProposalPDF action will depend on how your routing is configured, but assuming the defaults, the action route value will be ProposalPDF, the controller route value will be the name of the controller class in which this action resides (minus the Controller suffix) and the area will be the area folder in which the controller lives, with a value of empty string if in the default controller folder. Then using these route values, a view will be looked up in the Views folder using the following convention
~/Views/{Area}/{Controller}/{View}.cshtml
There is always Glimpse that can help with providing runtime Diagnostics too, such as which View file was used to serve up the returned page, although I'm not sure how this would look when a ViewResult is executed internally to provide the contents of a file.