Extending HtmlHelper RouteLink - c#

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);

Related

How to generate a link to a html page with parameters WebApi

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);
}

Roslyn add new method to an existing class

I'm investigating the use of the Roslyn compiler within a Visual Studio Extension (VSIX) that uses the VisualStudioWorkspace to update existing code. Having spent the last few days reading up on this, there seem to be several ways to achieve this....I'm just not sure which is the best approach for me.
Okay, so let's assume that the User has their solution open in Visual Studio 2015. They click on my Extension and (via a form) they tell me that they want to add the following method definition to an interface:
GetSomeDataResponse GetSomeData(GetSomeDataRequest request);
They also tell me the name of the interface, it's ITheInterface.
The interface already has some code in it:
namespace TheProjectName.Interfaces
{
using System;
public interface ITheInterface
{
/// <summary>
/// A lonely method.
/// </summary>
LonelyMethodResponse LonelyMethod(LonelyMethodRequest request);
}
}
Okay, so I can load the Interface Document using the following:
Document myInterface = this.Workspace.CurrentSolution?.Projects?
.FirstOrDefault(p
=> p.Name.Equals("TheProjectName"))
?.Documents?
.FirstOrDefault(d
=> d.Name.Equals("ITheInterface.cs"));
So, what is the best way to now add my new method to this existing interface, ideally writing in the XML comment (triple-slash comment) too? Bear in mind that the request and response types (GetSomeDataRequest and GetSomeDataResponse) may not actually exist yet. I'm very new to this, so if you can provide code examples then that would be terrific.
UPDATE
I decided that (probably) the best approach would be simply to inject in some text, rather than try to programmatically build up the method declaration.
I tried the following, but ended up with an exception that I don't comprehend:
SourceText sourceText = await myInterface.GetTextAsync();
string text = sourceText.ToString();
var sb = new StringBuilder();
// I want to all the text up to and including the last
// method, but without the closing "}" for the interface and the namespace
sb.Append(text.Substring(0, text.LastIndexOf("}", text.LastIndexOf("}") - 1)));
// Now add my method and close the interface and namespace.
sb.AppendLine("GetSomeDataResponse GetSomeData(GetSomeDataRequest request);");
sb.AppendLine("}");
sb.AppendLine("}");
Inspecting this, it's all good (my real code adds formatting and XML comments, but removed that for clarity).
So, knowing that these are immutable, I tried to save it as follows:
var updatedSourceText = SourceText.From(sb.ToString());
var newInterfaceDocument = myInterface.WithText(updatedSourceText);
var newProject = newInterfaceDocument.Project;
var newSolution = newProject.Solution;
this.Workspace.TryApplyChanges(newSolution);
But this created the following exception:
bufferAdapter is not a VsTextDocData
at Microsoft.VisualStudio.Editor.Implementation.VsEditorAdaptersFactoryService.GetAdapter(IVsTextBuffer bufferAdapter)
at Microsoft.VisualStudio.Editor.Implementation.VsEditorAdaptersFactoryService.GetDocumentBuffer(IVsTextBuffer bufferAdapter)
at Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem.InvisibleEditor..ctor(IServiceProvider serviceProvider, String filePath, Boolean needsSave, Boolean needsUndoDisabled)
at Microsoft.VisualStudio.LanguageServices.RoslynVisualStudioWorkspace.OpenInvisibleEditor(IVisualStudioHostDocument hostDocument)
at Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem.DocumentProvider.StandardTextDocument.UpdateText(SourceText newText)
at Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem.VisualStudioWorkspaceImpl.ApplyDocumentTextChanged(DocumentId documentId, SourceText newText)
at Microsoft.CodeAnalysis.Workspace.ApplyProjectChanges(ProjectChanges projectChanges)
at Microsoft.CodeAnalysis.Workspace.TryApplyChanges(Solution newSolution)
at Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem.VisualStudioWorkspaceImpl.TryApplyChanges(Solution newSolution)
If I were you I would take advantage of all Roslyn benefits, i.e. I would work with the SyntaxTree of the Document rather than processing the files text (you are able to do the latter without using Roslyn at all).
For instance:
...
SyntaxNode root = await document.GetSyntaxRootAsync().ConfigureAwait(false);
var interfaceDeclaration = root.DescendantNodes(node => node.IsKind(SyntaxKind.InterfaceDeclaration)).FirstOrDefault() as InterfaceDeclarationSyntax;
if (interfaceDeclaration == null) return;
var methodToInsert= GetMethodDeclarationSyntax(returnTypeName: "GetSomeDataResponse ",
methodName: "GetSomeData",
parameterTypes: new[] { "GetSomeDataRequest" },
paramterNames: new[] { "request" });
var newInterfaceDeclaration = interfaceDeclaration.AddMembers(methodToInsert);
var newRoot = root.ReplaceNode(interfaceDeclaration, newInterfaceDeclaration);
// this will format all nodes that have Formatter.Annotation
newRoot = Formatter.Format(newRoot, Formatter.Annotation, workspace);
workspace.TryApplyChanges(document.WithSyntaxRoot(newRoot).Project.Solution);
...
public MethodDeclarationSyntax GetMethodDeclarationSyntax(string returnTypeName, string methodName, string[] parameterTypes, string[] paramterNames)
{
var parameterList = SyntaxFactory.ParameterList(SyntaxFactory.SeparatedList(GetParametersList(parameterTypes, paramterNames)));
return SyntaxFactory.MethodDeclaration(attributeLists: SyntaxFactory.List<AttributeListSyntax>(),
modifiers: SyntaxFactory.TokenList(),
returnType: SyntaxFactory.ParseTypeName(returnTypeName),
explicitInterfaceSpecifier: null,
identifier: SyntaxFactory.Identifier(methodName),
typeParameterList: null,
parameterList: parameterList,
constraintClauses: SyntaxFactory.List<TypeParameterConstraintClauseSyntax>(),
body: null,
semicolonToken: SyntaxFactory.Token(SyntaxKind.SemicolonToken))
// Annotate that this node should be formatted
.WithAdditionalAnnotations(Formatter.Annotation);
}
private IEnumerable<ParameterSyntax> GetParametersList(string[] parameterTypes, string[] paramterNames)
{
for (int i = 0; i < parameterTypes.Length; i++)
{
yield return SyntaxFactory.Parameter(attributeLists: SyntaxFactory.List<AttributeListSyntax>(),
modifiers: SyntaxFactory.TokenList(),
type: SyntaxFactory.ParseTypeName(parameterTypes[i]),
identifier: SyntaxFactory.Identifier(paramterNames[i]),
#default: null);
}
}
Note that this is pretty raw code, Roslyn API is extremely powerful when it comes to analyzing/processing the syntax tree, getting symbol information/references and so on. I would recommend you to look at this page and this page for reference.

in Razor view calling method of another controller and passing parameters

In a view of ClimateChartController I do this code:
#Html.ActionLink("Kies land", "ListCountries", "Continent" new {Selectedyear = #ViewBag.SchoolYear, continentId = #ViewBag.ContinentId})
So this should go to the method ListCountries of ContinentController, along with the given parameters.
Now this doesn't work, if I do it without the parameters it goes to the method but well, I need the parameters...
For now I resolved this by using the following method in ClimateChartController:
public ActionResult ListCountries(int selectedyear, int continentid)
{
return RedirectToAction("ListCountries", "Continent",
new { selectedYear = selectedyear, continentId = continentid });
}
This works as intended, but causes cluttering of code and isn't neat.
So how can I call a method of another controller and pass some parameters with it?
Try this:
#Html.ActionLink("Kies land", "ListCountries", "Continent" , null, new {Selectedyear = #ViewBag.SchoolYear, continentId = #ViewBag.ContinentId})
OR:
Html.ActionLink("Kies land", "ListCountries", "Continent", new {Selectedyear = #ViewBag.SchoolYear, continentId = #ViewBag.ContinentId}, null)
There are possible solutions here:
Why does Html.ActionLink render "?Length=4"

Serve image through ajax in html helper

I am curios about a thing. Let's say I am saving images in a SQL Database ( I know it's not recommended; the best way it's to save only a reference to a image saved somewhere else, but I want to ask you something about this specific case ).
I am serving a file like this :
public ActionResult Serve(int id)
{
......
return File(img.Content, img.ContentType);
}
I have also made an Html helper :
public static HtmlString ServeImage(this HtmlHelper html, int id)
{
var urlHelper= new UrlHelper(html.ViewContext.RequestContext);
var tag= "<img src='{0}' width='200' height='200' />";
return new HtmlString(string.Format(imageTag, urlHelper.Action("Serve", "Image", new { id = id })));
}
So, when I want to show a picture I am writing in a view something like this: #Html.ServeImage(imageId)
My question is: *Is there any way yo call urlHelper.Action("Serve", "Image", new { id = id }))) through ajax and still use my helper? *
I have read about Ajax Helpers but I think it doesn't help me here and I have only one option left. I need to give up my helper and call my action with ajax like I normally do. Is this right?
I mean:
$.ajax(function() {
.....
});
You can just use a plain img tag and set the src to #urlHelper.Action("Serve", "Image", new { id = id })))
Example:
<img src='#urlHelper.Action("Serve", "Image", new { id = id })' width='200' height='200' />

How to use MVC 3 #Html.ActionLink inside c# code

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");

Categories