MVC User Controls

Contents:

  1. Introduction
  2. Partial views
  3. Custom HtmlHelper methods
  4. Templated helpers
  5. Summary

1. Introduction Go top

The concept of user controls as we were used in the Web Forms projects doesn’t exist in MVC but we have three simmilar alternatives:

  1. Partial views
  2. HtmlHelper extentions
  3. Templated helpers

2. Partial views Go top

Partial views are similar in concept to normal views with the main difference that they can be embedded inside views. As an example, we will create a page that, for the currently logged user, displays the billing and the shipping address allowing the user to edit them.

Final result

We will continue the solution build in the previous posts, that you can download from Codeplex. Or, if you prefer, you can download directly the source code for the current post using this link.

First we will create a new table to store the addresses and we will modify the users table to have two references to the Addresses table, one for the billing and one for the shipping address.

Create a new table for storing addresses

Below is the SQL script for updating the DB:

CREATE TABLE [dbo].[Addresses](
	[AddressId] [int] IDENTITY(1,1) NOT NULL,
	[Street] [varchar](100) NOT NULL,
	[ZipCode] [varchar](50) NULL,
	[City] [varchar](100) NOT NULL,
	[Country] [varchar](100) NOT NULL,
 CONSTRAINT [PK_Addresses] PRIMARY KEY CLUSTERED
(
	[AddressId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
BEGIN TRANSACTION
SET QUOTED_IDENTIFIER ON
SET ARITHABORT ON
SET NUMERIC_ROUNDABORT OFF
SET CONCAT_NULL_YIELDS_NULL ON
SET ANSI_NULLS ON
SET ANSI_PADDING ON
SET ANSI_WARNINGS ON
COMMIT
BEGIN TRANSACTION
GO
ALTER TABLE dbo.Addresses SET (LOCK_ESCALATION = TABLE)
GO
COMMIT
BEGIN TRANSACTION
GO
ALTER TABLE dbo.Users ADD
	BillingAddressId int NULL,
	ShippingAddressId int NULL
GO
ALTER TABLE dbo.Users ADD CONSTRAINT
	FK_Users_ShippingAddresses FOREIGN KEY
	(
	ShippingAddressId
	) REFERENCES dbo.Addresses
	(
	AddressId
	) ON UPDATE  NO ACTION
	 ON DELETE  NO ACTION

GO
ALTER TABLE dbo.Users ADD CONSTRAINT
	FK_Users_BillingAddresse FOREIGN KEY
	(
	BillingAddressId
	) REFERENCES dbo.Addresses
	(
	AddressId
	) ON UPDATE  NO ACTION
	 ON DELETE  NO ACTION

GO
ALTER TABLE dbo.Users SET (LOCK_ESCALATION = TABLE)
GO
COMMIT

First we will update the entity framework model to include a new entity for the Address. Then we will create a partial view that is able to display an address in read-only or edit mode. For knowing if we are in read-only mode we will add a new property (IsReadOnly) to the Address partial class:

using System.ComponentModel.DataAnnotations;

namespace HelloWorld.Code.DataAccess
{
    [MetadataType(typeof(AddressMetaData))]
    public partial class Address
    {
        private bool _isReadOnly = true;
        public bool IsReadOnly
        {
            get { return _isReadOnly; }
            set { _isReadOnly = value; }
        }
    }

    public class AddressMetaData
    {
        [Required(ErrorMessage = "Required")]
        [StringLength(100, MinimumLength = 3, ErrorMessage = "The street must be a string of at least 3 characters and at most 100 characters")]
        [Display(Name = "Street")]
        public string Street { get; set; }

        [StringLength(50, ErrorMessage = "The ZIP code can't have more than 50 characters")]
        [Display(Name = "ZIP Code")]
        public string ZipCode { get; set; }

        [Required(ErrorMessage = "Required")]
        [StringLength(100, MinimumLength = 3, ErrorMessage = "The city must be a string of at least 3 characters and at most 100 characters")]
        [Display(Name = "City")]
        public string City { get; set; }

        [Required(ErrorMessage = "Required")]
        [StringLength(100, MinimumLength = 3, ErrorMessage = "The country must be a string of at least 3 characters and at most 100 characters")]
        [Display(Name = "Country")]
        public string Country { get; set; }
    }
}

We are ready now to create a partial view that will use the Address class as a model. Just like in the case of normal views, the partial views must be stored under the ‘Views’ folder. Because I like to store all my partial views inside a single folder I created a new sub-folder under ‘Views’ called ‘PartialViews’. Then, for adding a partial view, lets make a right click on the ‘PartialViews’ folder in the solution explorer and click ‘Add’ » ‘View’. In the pop-up that opens make sure to tick the check-box ‘Create as a partial view’:

Create a partial view for the address

We will make our partial view strongly typed by specifying, in the first line, the type of the model this partial view works with:

@model HelloWorld.Code.DataAccess.Address

And we will display the address in read-only or edit mode depending on the current value for the model property: @Model.IsReadOnly.

@model HelloWorld.Code.DataAccess.Address

@if(Model == null)
{
    @:None
    <a href="#" class="edit-address">Edit</a>  
}
else if (Model.IsReadOnly)
{
    @Html.LabelFor(model => model.Street)@: <strong>@Model.Street</strong>
    <br/>
    @Html.LabelFor(model => model.ZipCode)@: <strong>@Model.ZipCode</strong>
    <br/>
    @Html.LabelFor(model => model.City)@: <strong>@Model.City</strong>
    <br/>
    @Html.LabelFor(model => model.Country)@: <strong>@Model.Country</strong>
    <br/>
    <a href="#" class="edit-address">Edit</a>          
}
else
{
    @Html.ValidationSummary(true)
 
    @Html.LabelFor(model => model.Street, new { @class = "control-label" })
    @Html.TextBoxFor(model => model.Street)
    @Html.ValidationMessageFor(model => model.Street)
    <br/>
    @Html.LabelFor(model => model.ZipCode, new { @class = "control-label" })
    @Html.TextBoxFor(model => model.ZipCode)
    @Html.ValidationMessageFor(model => model.ZipCode)
    <br/>
    @Html.LabelFor(model => model.City, new { @class = "control-label" })
    @Html.TextBoxFor(model => model.City)
    @Html.ValidationMessageFor(model => model.City)
    <br/>
    @Html.LabelFor(model => model.Country, new { @class = "control-label" })
    @Html.TextBoxFor(model => model.Country)
    @Html.ValidationMessageFor(model => model.Country)
    <br/>
    <a href="#" class="save-address">Save</a>     
}

Next we will create a controller called UserControlsController and a view called PartialViewExample that will display the name and surname of the currently logged user plus the shipping and billing addresses using the new partial view we just created.

@model HelloWorld.Code.DataAccess.User
@{
    ViewBag.Title = "PartialView Example";
}
<h2>@ViewBag.Title</h2>
<p>You are logged in as <strong>@Model.FirstName @Model.LastName</strong>.</p>
<hr/>
<p>Billing address:</p>
<div id="BillingAddress">
    @Html.Partial("~/Views/PartialViews/AddressPartialView.cshtml", new ViewDataDictionary(Model.BillingAddress))
</div>
<hr/>
<p>Shipping address:</p>
<div id="ShippingAddress">
    @Html.Partial("~/Views/PartialViews/AddressPartialView.cshtml", new ViewDataDictionary(Model.ShippingAddress))
</div>

For inserting a partial view we make use of a helper method Html.Partial and we pass the model to use through a ViewDataDictionary object. We are using the ViewDataDictionary to pass the model to the partial view even if the HtmlHelper class has an overload method for the Partial method that receives as an input the partial view name and the model to pass to the partial view. We do this because the latter method has a curious behavior: in case the passed object is null, it will take the model of the current view and pass it to the partial view instead of passing a null value. So, if we would include the partial view using this method:

@Html.Partial("PartialViews/AddressPartialView", Model.ShippingAddress)

And the current user doesn’t have a shipping address, the Partial method will pass the view’s model which in our care is a User object and we will get a strange run-time error saying that:

The model item passed into the dictionary is of type ‘HelloWorld.Code.DataAccess.User’, but this dictionary requires a model item of type ‘HelloWorld.Code.DataAccess.Address’.

Below is the code for the controller that retrieves the data for the logged user and passes it to the view:

using System.Web.Mvc;
using System.Linq;
using System.Data.Entity;
using HelloWorld.Code.DataAccess;
using HelloWorld.Code.Security;

namespace HelloWorld.Controllers
{
    [Authorize]
    public class UserControlsController : Controller
    {
        public ActionResult PartialViewExample()
        {
            var userId = HttpContext.User.ToCustomPrincipal().CustomIdentity.UserId;
            using (var context = new MvcDemoEntities())
            {
                var user = context.Users.Include(u => u.ShippingAddress).Include(u => u.BillingAddress).First(u => u.UserId == userId);
                return View(user);
            }
        }
    }
}

If you run now the code, you will see that the address partial view was inserted twice in the view.

In practice views are great when updating content via Ajax requests, and this is what we are going to do next. When the user clicks on Edit we will replace the read-only text of the address with the edit-enabled version of the address partial view.

For this we need some JavaScript to make the Ajax calls so we will add the following script inside the view in the scripts section:

@section scripts
{
    <script type="text/javascript">
        $().ready(function () {
            Init();
        });

        function Init() {
            $(".edit-address").click(function (event) {
                event.preventDefault();
                var link = $(this);
                var container = link.parent();

                $.ajax({
                    url: '@Url.Action("EditLoggedUserAddress", "UserControls")',
                    data: { type: container.attr('id') },
                    type: 'GET',
                    timeout: 300000, //5 minutes
                    dataType: 'html',

                    success: function (response) {
                        container.html(response);
                        Init();
                    },
                    error: function () {
                        alert('Error');
                    }
                });
            });
        }
    </script>
}

Note: Sections are not supported inside partial views. That’s why I added the JavaScript code inside the view but a better place to put this script would be in an external JavaScript file.

On the server side, we need a new controller action method that returns the HTML for the partial view when the user clicks on ‘Edit’.

[HttpGet]
public ActionResult EditLoggedUserAddress(string type)
{
	var userId = HttpContext.User.ToCustomPrincipal().CustomIdentity.UserId;
	using (var context = new MvcDemoEntities())
	{
		var user = context.Users.Include(u => u.ShippingAddress).Include(u => u.BillingAddress).First(u => u.UserId == userId);
		var address = string.Compare(type, "ShippingAddress", true) == 0 ? user.ShippingAddress : user.BillingAddress;
		if (address == null)
			address = new Address();

		address.IsReadOnly = false;
		return PartialView("~/Views/PartialViews/AddressPartialView.cshtml", address);
	}
}

The method that renders the partial view is PartialView. We use it to generate the HTML markup only for the AddressPartialView so we can return it to our Ajax caller that will update the HTML content only for that partial view.

Now we need a method for saving the entered data. We will do this again via Ajax calls, adding the following at the end of our Init function inside the view:

$(".save-address").click(function (event) {
	event.preventDefault();
	var link = $(this);
	var container = link.parent();

	$.ajax({
		url: '@Url.Action("SaveLoggedUserAddress", "UserControls")',
		data: container.children("input, textarea, select").serialize() + "&type=" + container.attr('id'),
		type: 'POST',
		timeout: 300000, //5 minutes
		dataType: 'html',

		success: function (response) {
			container.html(response);
			Init();
		},
		error: function () {
			alert('Error');
		}
	});
});

The controller method that will actually save the data is:

[HttpPost]
public ActionResult SaveLoggedUserAddress(Address addressModel, string type)
{
	if (!ModelState.IsValid)
	{
		addressModel.IsReadOnly = false;
		return PartialView("~/Views/PartialViews/AddressPartialView.cshtml", addressModel);
	}

	var userId = HttpContext.User.ToCustomPrincipal().CustomIdentity.UserId;
	var isShippingAddress = System.String.Compare(type, "ShippingAddress", System.StringComparison.OrdinalIgnoreCase) == 0;
	using (var context = new MvcDemoEntities())
	{
		var user = context.Users.Include(u => u.ShippingAddress).Include(u => u.BillingAddress).First(u => u.UserId == userId);
		var address = isShippingAddress ? user.ShippingAddress : user.BillingAddress;
		if (address == null)
		{
			address = addressModel;
			context.AddToAddresses(address);
			if (isShippingAddress)
				user.ShippingAddress = address;
			else
				user.BillingAddress = address;
		}

		address.Street = addressModel.Street;
		address.ZipCode = addressModel.ZipCode;
		address.City = addressModel.City;
		address.Country = addressModel.Country;
		context.SaveChanges();

		address.IsReadOnly = true;
		return PartialView("~/Views/PartialViews/AddressPartialView.cshtml", address);
	}
}

This is it. If you want to download the complete example you can find it here on Codeplex.

3. Custom HtmlHelper methods Go top

Filtered text boxA common way of including dynamically generated HTML markup inside a view is by using the methods defined in the HtmlHelper class. This class contains helper classes that can be used to generate basic HTML elements like text boxes, text areas and check boxes. Apart from the default implementation we can write our own custom helper methods. I explained in detail how this can be done in another post: Custom HtmlHelper for filtering the input in a text box.

4. Templated helpers Go top

[Available soon]

5. Summary Go top

For a comprehensive tutorial check this link: MSDN: Walkthrough: Using Templated Helpers to Display Data in ASP.NET MVC.

Advertisements

6 comments on “MVC User Controls

  1. Again, very nice post. Congratulations! Why not use an HtmlHelper custom function to do a strong typed Partial? something like @Html.Partial(“View”,Model) ? Thus you may even be able to validate the type of the model based on the declaration in the view.

  2. First let me say I’m happy you’re reading those posts since you are the best “programmer-teacher-colleague” I ever had. Now that I finished reminding you what a great guy you are I’ll try to answer 🙂

    The partial view is already strongly typed, that’s because of the first line of code: @model HelloWorld.Code.DataAccess.Address
    If I’d try to pass to the partial view a different type of model, our good friend Resharper, would signal the problem immediately.

    Anyway, I agree with you, it would be easier to write a custom HtmlHelper extension for rendering the partial view that, when passing null @Html.CustomPartial(“PartialView”, null) would not use the model of the current view but would actually use null.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s