Note: This post is out of date, please see the updated version here
If you really want to read it, please be my guest but this method is unnecessary and @Html.Raw presents many XSS security issues.
TL;DR:
This one is a little different than my other posts. Basically, I wrote some code that I’m happy about that allows me to edit data in a very UX/UI friendly fashion by passing Razor model data into a JS function.
What I wanted to do:
This post isn’t going to be as long as my normal ones. I’m just excited to share some code I figured out today. I’ve been working on an issue that I came across while trying to create an Admin page. What I want to do is display all of my users on the page in a table like structure with edit and delete buttons for each one. So before I get into the problem and how I solved it, let’s look at what I wanted to achieve:
- Keep each user row template in it’s own partial view for better manageability
- Same goes for the edit user row template
- Adhere to as many best practices as possible
- Render scripts in the scripts section of the layout page, rather than wherever is convenient
- Use view models appropriately
- Pull view data from the Controller
- Valid HTML
- No page refresh, so everything is editable in-place
The Model Problem…
Because I wanted to keep my views as independent as possible, I had a hard time juggling the actual model data that came from my Controller. Since sections from partial views aren’t rendered into the final view, getting the correct model data into a JavaScript function was tricky. I also wanted it to be as clean as possible. There were hacky methods I could have used but I’m trying to be better than that. There’s also a second problem. JavaScript essentially only handles one submit action for a form. In order for my user rows to have an Edit button AND a Delete button, I had to figure out how to handle that as well.
The Solution on the HTML side:
This is part of the partial view for a single user row:
@model UserManagementSystem.Models.UserViewModel <form action="" method="post" data-ref="@Model.Name"> <div> @Html.DisplayFor(m => m.Name) <input name="Name" type="hidden" value="@Model.Name" /> </div> <div> @Html.DisplayFor(m => m.Role) <button type="button" data-ref="@Model.Name" onclick='Edit(this,@Html.Raw(Json.Encode(Model)));' name="action" value="edit" class="no-default"> <span class="glyphicon glyphicon-edit"> </span> </button> </div> <div> <button type="submit" name="action" value="delete" class="no-default"> <span class="glyphicon glyphicon-remove"> </span> </button> </div> </form>
The extra divs allow me to display this form like a row in a table, but there’s some extra CSS that goes along with that to make it work. If you’d like me to go into detail about that, let me know and I might write another post about it but I don’t want to muddy this up any further.
If you’re familiar with ASP.NET MVC, much of the above code should be self explanatory. But if you’re not, the @model UserManagementSystem.Models.UserViewModel
is a Razor line that holds the model data that came from the controller. The @Html.DisplayFor
lines are also Razor syntax that are called HTML helpers that create the necessary HTML markup for the item(s) from the data model that are passed as parameters. Then there are the html input tags and button tags. HTML helpers do exist for creating these input tags BUT they have a nasty habit of automatically assigning IDs to the element that are not easily controllable. They work perfectly fine when you’re only displaying one item on a page, but in this case, I needed to have this form appear many times for different users on the same page. Ergo, I wrote those tags manually, without even needing to give each one and ID.
The part that matters is the onclick
action for the edit button.
<button type="button" data-ref="@Model.Name" onclick='Edit(this,@Html.Raw(Json.Encode(Model)));' name="action" value="edit" class="no-default">
With that statement, I’m passing the button (which allows me to have access to all of the data attributes associated with the button) along with the model data that was passed into the view originally. @Html.Raw(Json.Encode(Model))
is another HTML helper line that uses Json.Encode
to encode the model data as a JSON object. The @Html.Raw
function prevents the Json.Encode
function from automatically replacing certain symbols that may appear in your model data with more HTML friendly ones.
The Solution on the JS/jQ side:
Now the JavaScript/jQuery functions in the parent View:
@section scripts{ <script> function Edit(button, model) { var model = new UserViewModel(model); var formRef = $(button).attr("data-ref") $.ajax({ url: "@Url.Action("EditUser")", data: model, success: function (data) { var ref = "form[data-ref='"+formRef+"']"; $(ref).html(data); } }) } function UserViewModel(model) { this.Name = model.Name; this.Role = model.Role; } </script> }
This is a pretty standard JS snippet. The Edit function creates a new JS UserViewModel based off the model data that was passed into the function, gets the data attribute in the button that tells us where to find the form we want to update with the new HTML data that is returned from our controller, and finally calls our controller action with an ajax function to get the EditUser view data that will replace the current HTML in the form.
Because this code exists within the root view, I can render this script as a section that will be rendered correctly in the _Layout shared view. If it were in the view that displays a user row, it would not be rendered at all since that particular view is rendered as a child. I could have put the code directly inside a <script></script> block on that view BUT that code would be rendered before the jQuery framework was loaded into the document, throwing all sorts of errors. Why not just load the jQuery framework first? Well, that would increase the loading time of the page and I would violate the best practice of loading all JS at the end of the document.
In Conclusion:
Did I achieve what I set out to?
- Keep each user row template in it’s own partial view for better manageability
- Yes. I can change the user row view however I need to and as long as it is a child view to the nth-degree of the view that contains the necessary JS, it will work.
- Same goes for the edit user row template
- Yes. I didn’t cover the edit user view because it’s very similar to the user row view
- Adhere to as many best practices as possible
- Render scripts in the scripts section of the layout page, rather than wherever is convenient
- Yes. Since I was able to pass the required model data up to the parent view, the JS is not required to be in the same view as the model.
- Use view models appropriately
- Maybe. I think I did? If I didn’t, please let me know! It seemed a little superfluous to create a JS object to represent a model that was already being passed as a JSON object, but by doing so, I think I can ensure that only the data I specifically bind to in that JS object will be passed to my controller, even if extra data was maliciously inserted into the original model data somehow.
- Pull view data from the Controller
- Yes. All of my view data is pulled from the controller instead of cheating with
@Html.Partial
. Not that there is never a legitimate use for that, but I don’t feel like this was one of them.
- Yes. All of my view data is pulled from the controller instead of cheating with
- Valid HTML
- Yes. By using divs and CSS to display my data as a pseudo-table instead of using
<table></table>
, I am able to keep my form markup valid. Also, because I wrote out my input tags instead of letting Visual Studio “help” me out, I avoided duplicate element IDs.
- Yes. By using divs and CSS to display my data as a pseudo-table instead of using
- Render scripts in the scripts section of the layout page, rather than wherever is convenient
- No page refresh, so everything is editable in-place
- Yes. Thank you ajax!
What could be better?
- This method requires some slightly obtrusive JS to handle the onclick event of the button. However, I can’t think of a non-obtrusive way that would be clean enough to warrant the change.
- While this solution hinges on
@Html.Raw(Json.Encode())
, it feels a little hacky. It works well here, but I don’t know every security implication that comes along with it. If anyone wants to enlighten me, I would greatly appreciate that.
thanks alot you solved my problem!!