2

We're in the process of transferring a big WebForms app to MVC.

In the WebForms app we have some reusable controls (.ascx). For instance, a TextBox that shows username suggestions as you type. The control adds some JS/CSS to the page, has some server-side logic and custom properties (like SelectedUserID).

What should it be in an MVC app? How do I encapsulate those things into one reusable entity in an MVC app? Maybe a partial view? But you can't add, say, JS/CSS to the page's <head> from a partial view (preferably, with a check that it's not already been added)... Also, how do I return something like the mentioned SelectedUserID in a partial view?

To rephrase the question - how would you implement such a control in an MVC app?

PS. I know you can still use .ascx user-controls in MVC apps but it seem a "hacky/legacy" way. What is the "legitimate" option?

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Alex from Jitbit
  • 53,710
  • 19
  • 160
  • 149

6 Answers6

4

Your question is pretty broad. All my comments/answer are going to be in Razor. In my opinion, if you're going to switch to MVC, you might as well use Razor. There are plenty of good reasons to switch, but I'd say my top pick for anyone who is migrating, the best reason is two fold; first razor allowed me to drop some bad habits about how programming works with webforms and in the same manor it forced me to re-write my code instead of trying to copy and paste and change the original code to work in MVC.

The control adds some JS/CSS to the page, has some server-side logic and custom properties. What should it be in an MVC app?

This is a pretty religious software question. There are plenty of ways of doing it.

So onto my religous opinion about how to add JS/CSS in MVC. One way to include server side is to create a region in the Layout/Master template.

Views/_ViewStart.cshtml

@{
  Layout = "~/Views/Shared/Layout.cshtml";
}

Views/Shared/Layout.cshtml

<!doctype html>
  <head>
    <link href="/Styles/Global.css" rel="stylesheet" type="text/css" />
    @RenderSection("Styles", required: false)
    <script src="/Scripts/Global.js" type="text/javascript"></script>
    @RenderSection("Scritps", required: false)

This would allow each view to optionally (because required=false) add any Styles or Scripts using the RenderSecion code.

Views/Home/Index.cshtml

@section Styles {
    <link href="/Styles/Home/Index.css" rel="stylesheet" type="text/css" />
}

This is pretty simple, and probably a good solution for many simple to moderately complex sites. This wasn't enough for me, as I needed to do what you requested, and only include files if they were truly needed. Additionally, partial views and templates cannot render sections, which I found to be a giant PITA. So I added some HtmlHelper extension methods. (i'm only going to show the code for Javascript as the Stylesheets are nearly Identical code. These methods don't allow duplicate urls.

Domain/HtmlHelperExtensions.cshtml

public static class HtmlHelperExtensions
{
    private const string _JSViewDataName = "RenderJavaScript";
    private const string _StyleViewDataName = "RenderStyle";

    public static void AddJavaScript(this HtmlHelper HtmlHelper, string ScriptURL)
    {
        List<string> scriptList = HtmlHelper.ViewContext.HttpContext.Items[HtmlHelperExtensions._JSViewDataName] as List<string>;
        if (scriptList != null)
        {
            if (!scriptList.Contains(ScriptURL))
            {
                scriptList.Add(ScriptURL);
            }
        }
        else
        {
            scriptList = new List<string>();
            scriptList.Add(ScriptURL);
            HtmlHelper.ViewContext.HttpContext.Items.Add(HtmlHelperExtensions._JSViewDataName, scriptList);
        }
    }

    public static MvcHtmlString RenderJavaScripts(this HtmlHelper HtmlHelper)
    {
        StringBuilder result = new StringBuilder();

        List<string> scriptList = HtmlHelper.ViewContext.HttpContext.Items[HtmlHelperExtensions._JSViewDataName] as List<string>;
        if (scriptList != null)
        {
            foreach (string script in scriptList)
            {
                result.AppendLine(string.Format("<script type=\"text/javascript\" src=\"{0}\"></script>", script));
            }
        }

        return MvcHtmlString.Create(result.ToString());
    }
}

Updated Views/Shared/Layout.cshtml

<!doctype html>
  <head>
    <link href="/Styles/Global.css" rel="stylesheet" type="text/css" />
    @Html.RenderStyleSheets()
    <script src="/Scripts/Global.js" type="text/javascript"></script>
    @Html.RenderJavascripts()

Updated Views/Home/Index.cshtml

@Html.AddStyleSheet("/Styles/Home/Index.css")

Onto your next question...

How do I encapsulate those things into one reusable entity in an MVC app? Maybe a partial view?

Razor supports both partial views and templates. Although technically there is large overlap in what each can do, there are limitations of how each were designed to allow a programmer to take advantage of each when needed.

Partial views do not require a Model/Class in order to be rendered. Here is a completely valid partial view:

/Views/Home/Index.cshtml

@Html.Partial("Partial-Menu")

/Views/Shared/Partial-Menu.cshtml

<div id="menu">
  <a href="/">Home</a>
  <a href="/Home/About">About Us</a>
</div>

Templates on the other had, do required a Model in order to be rendered. This is because Templates are a way to render any class or struct as either an Editing Template or a Display Template.

/Models/Person.cs

public class Person
{
  public string FirstName { get; set; }
  public string LastName { get; set; }
}

/Controllers/HomeController.cs

public ActionResult Index()
{
  Person model = new Person();
  model.FirstName = "Jon";
  model.LastName = "Doe";

  return this.View(model);
}

/Views/Home/Index.cshtml

@model Project1.Models.Person

@Html.DisplayModel()

@Html.EditforModel()

/Views/Shared/DisplayTemplates/Person.cshtml

@model Project1.Models.Person

<div>@Html.LabelFor(x => x.FirstName) @Html.DisplayFor(x => x.FirstName)</div>
<div>@Html.LabelFor(x => x.LastName) @Html.DisplayFor(x => x.LastName)</div>

/Views/Shared/EditorTemplates/Person.cshtml

@model Project1.Models.Person

<div>@Html.LabelFor(x => x.FirstName) @Html.EditorFor(x => x.FirstName)</div>
<div>@Html.LabelFor(x => x.LastName) @Html.EditrorFor(x => x.LastName)</div>

The way I see it, any model that might turn into a Form for data entry should probably be a Template. This is the way I prefer to encapsulate my models. Using the extension methods provided earlier, both my partial views and templates can load includes as needed (currently only one of my models of oodles of them actually needed to use this).

My preference is to have up to three includes (of each Javascript and Styles) per page rendered. I basically have a Global.css/js, a controller Home.css/js, and a page Index.css/js as the possible includes. It's been very seldom that I have a controller include.

Erik Philips
  • 53,428
  • 11
  • 128
  • 150
4

The best solution I've found so far is Razor declarative helpers. They fit awesomely.

@helper UserTextBox() {
    <input type="text" />

    <!--my own helper that registers scripts-->
    @Html.AddJS("../js/myscript.js") 
}

Related question: Razor: Declarative HTML helpers

Related note: you can't use @Html helper inside a declarative helper but there's a workaround: https://stackoverflow.com/a/5557503/56621

Community
  • 1
  • 1
Alex from Jitbit
  • 53,710
  • 19
  • 160
  • 149
1

Well, you can't have something that "automatically" includes css/js. That's something the user has to add to the page (either the master/layout or the current page). But in general, one would create an Html Helper. Don't expect these to be complex systems (like the gigantic grids of asp.net days) but you can do a lot with them.

Yes, a partial page may be easier for simple things. Even simpler might be an Editor or Display Template.

In general, however, most "controls" for MVC these days are jQuery based.

A short video is available here: http://www.asp.net/mvc/videos/mvc-2/how-do-i/how-do-i-create-a-custom-html-helper-for-an-mvc-application

Don't be tempted to include your javascript in these mechanisms. While it may work for a single control, if you have multiple controls on a page you will get duplicate css/js.

Erik Funkenbusch
  • 92,674
  • 28
  • 195
  • 291
  • +1 for not including javascript this way. Actually i prefer to let the page to decide which javascript to include not the PartialViews. – Wahid Bitar May 31 '12 at 13:07
1

I would add the js/css to the main layout then write an html helper to render your custom control.

Many controls that uses jquery in aspnet mvc works this way.

Take a look at this controls here

Diego Garcia
  • 1,134
  • 1
  • 12
  • 25
0

1) for custom js/css you can use sections like this:

//code in view

@section someName
{
    ...
}

//code in layoutpage

@if (IsSectionDefined("someName"))
    {
        @RenderSection("someName")
    }

2) i'd better do the following:

  • create model SelectedUser with Id property
  • create templates for this model
  • add a SelectedUser object to any model, which needs it
arwyl
  • 1
0

They must be separated from surrounding text by blank lines. The begin and end tags of the outermost block element must not be indented. Markdown can't be used within HTML blocks.

ranu
  • 1
  • 1