DirigoEdge
Justin Colangelo
by Justin Colangelo
share this
?fl
« Back to the Blog

Client Editable 302 Redirects for Edge

03/26/2014
Client Editable 302 Redirects for Edge

Adding Client-Editable 302 Redirects to Your Edge Project

Sometimes admins want to write their own redirects. We don't want to touch the web.config file directly so we will do this programmatically when requests are made. This solution is specific to the DirigoEdge open-source CMS which is written in ASP.NET 4.5 C#.

Entity

Add an entity to your database.

 
	public class Redirect
    {
        [Key]
        public virtual int RedirectId { get; set; }
        public virtual DateTime DateModified { get; set; }
        [MaxLength(256)]
        public virtual String Source { get; set; }
        [MaxLength(256)]
        public virtual String Destination { get; set; }
        public virtual Boolean Active { get; set; }
    }

Context

Edit your DataContext file

	// Redirects
	public DbSet<Redirect> Redirects { get; set; }

Controller

Add a controller for Redirects

    public class RedirectController : Controller
    {
      [HttpPost]
      [PermissionsFilter(Permissions = "Can Edit Settings")]
      [AcceptVerbs(HttpVerbs.Post)]
      public JsonResult UpdateRedirects(IEnumerable<Redirect> entities)
      {
      	var result = new JsonResult();
      	if (entities != null)
      	{
      		using (var context = new DataContext())
      		{
      			foreach (var entity in entities)
      			{
      				// build out the url starting with / and ending with / changing spaces to -
      				var source = entity.Source.EndsWith("/") ? entity.Source.Replace(" ", "-") : entity.Source.Replace(" ", "-") + "/";
      				source = source.StartsWith("/") ? source : "/" + source;
      				Redirect editedRedirect = context.Redirects.FirstOrDefault(x => x.RedirectId == entity.RedirectId);
      				if (editedRedirect != null)
      				{
      					editedRedirect.DateModified = DateTime.Now;
      					editedRedirect.Source = source;
      					editedRedirect.Destination = entity.Destination;
      				}
      			}
      			result.Data = context.SaveChanges();
      		}
      	}
      // recycle cache after save
      var redirects = CachedObjects.GetRedirectsList(true);
      return result;
      }
    }
    
    

View Model

Add a new view model for Redirects

    public class RedirectViewModel
    {
      public List<Redirect> Redirects;
      public List<string> Pages = new List<string>(); 
      public RedirectViewModel()
      {
      	using (DataContext context = new DataContext())
      	{
      		Redirects = context.Redirects.ToList();
      		var pages = context.ContentPages.ToList();
      		foreach (var page in pages)
      		{
      			Pages.Add(Utils.NavigationUtils.GetGeneratedUrl(page));
      		}
      	}
      }
    }
    

View

	@model DirigoEdge.Areas.Admin.Models.ViewModels.RedirectViewModel
    @{
    	ViewBag.Title = "301 Redirect Settings";
    	Layout = "~/Areas/Admin/Views/Shared/_Layout.cshtml";    
    }
    <div class="siteSettings manageLift">
    	<form class="custom" data-list="true" method="POST" action="/admin/updateRedirects">
    		<div class="row">
    			<div class="twelve columns">
    				<h3>Redirects</h3>
    				<a id="NewRedirectPage" class="button mobileBlockStatic mobileMarginBottom" href="/admin/addredirect">New Redirect +</a>
    			</div>
    		</div>
    		<div class="row manageRedirect">
    			<table class="manageTable">
    				<thead>
    					<tr>
    						<th class="has-tip" title="/specials/win/<br/>/weekend/deals/<br/>/music/">Source Url</th>
    						<th class="has-tip" title="A list of created content pages">Destination Url</th>
    						<th></th>
    					</tr>
    				</thead>
    				<tbody>
    				@{
    					foreach(var redirect in Model.Redirects)
    					{
    					<tr>
    						<td class="source">
    							<input class="saveField" type="hidden" data-field="RedirectId" value="@redirect.RedirectId" />
    							<input class="saveField" type="text" data-field="Source" value="@redirect.Source" />
    						</td>
    						<td class="destination">
    							<select class="saveField no-custom" data-field="Destination">
    								<option value="/">/</option>
    								@{
    									foreach (var page in Model.Pages)
    									{
    										var url = "/content" + page;
    										<option value="@url">@page</option>
    									}
    								}
    							</select>
    						</td>
    						<td class="actions">
    							<a class="delete button secondary small tabletMarginBottom" href="javascript:void(0);" data-id="@redirect.RedirectId">Delete</a>
    						</td>
    					</tr>
    					}
    				}
    				</tbody>
    			</table>
    		</div>
            @*Save it*@
    		<div class="row">
    			<div class="two columns">
    				<a id="SaveRedirects" class="button mobileMarginBottom savePageButton" data-list="true" data-url="/redirect/updateredirects">Save</a>
    				<div id="SaveIndicator"></div>
    			</div>
    		</div>
    	</form>
    </div>
    @section Modals {
    	<div id="DeleteModal" class="reveal-modal">
    		<h2>Confirm Delete.</h2>
    		<p class="lead">Are you sure you want to delete this Redirect?</p>
    		<p class="">It will be <em>permanently</em> deleted.</p>
    		<a id="ConfirmRedirectDelete" class="right button mobileMarginBottom">Confirm</a>
    		<a class="right button secondary" onclick="$('#DeleteModal').trigger('reveal:close');">Cancel</a>
    		<a class="close-reveal-modal">&#215;</a>
    	</div>
    }
    @section Scripts {
    <script src="/Scripts/jquery/plugins/jquery.dataTables.min.js"></script>
    <script src="~/Scripts/jquery/plugins/jquery.dataTables.fixedHeader.js"></script>
    <script>
    	$(document).ready(function () {
    		// setup the datatable
    		var oTable = $("table.manageTable").dataTable({
    			"iDisplayLength": 25,
    			"aoColumnDefs": [
    			{ "bSortable": false, "aTargets": 		["actions"] } // No Sorting on actions
    			],
    			"aaSorting": [[0, "desc"]] // Sort by Source Url on load
    		});
    		// initialize fixed headers
    		new FixedHeader(oTable);
    		// this is a data list so we only save altered rows
    		$('table.manageTable').on('change', 'input, select', function() {
    			$(this).closest('tr').addClass('altered');
    		});
    		// Set the value to have no spaces and start with /
    		$('table.manageTable').on('keyup', '[data-field="Source"]', function (e) {
    			var $val = $(this);
    			var key = e.keyCode || e.which;
    			if (key === 32) {
    				$val.val($val.val().replace(' ', '-'));
    			}
    			if ($val.val().indexOf("/") != 0) {
    				$val.val('/' + $val.val());
    			}
    		});
    		// Add a new redirect line in the data table
    		$('#NewRedirectPage').click(function(e) {
    			e.preventDefault();
    			var url = $(this).attr('href');
    			// get the data for the new redirect
    			$.ajax({
    				url: url,
    				type: "POST",
    				dataType: 'json',
    				contentType: 'application/json; charset=utf-8',
    				success: function(data) {
    					// post the data to the table
    					var tableRow = [data.source, data.destination, data.action];
    					oTable.fnAddData(tableRow);
    					if (!$('.no-message').length) {
    						var noty_id = noty({ text: 'Added new redirect.', type: 'success', timeout: 3000 });
    					}
    					$("#SaveIndicator").hide();
    				},
    				error: function(data) {
    					if (!$('.no-message').length) {
    						var noty_id = noty({ text: 'There was an error processing your request.', type: 'error' });
    					}
    					$("#SaveIndicator").hide();
    				}
    			});
    		});
    	});
    	</script>
    }

Javascript Class

redirect_class = function() {
};
redirect_class.prototype.initRedirectEvents = function() {
	this.initDeleteRedirectEvent();
};
redirect_class.prototype.initDeleteRedirectEvent = function() {
	var self = this;
	$("div.manageRedirect table.manageTable").on('click', 'a.delete', function () {
		// ID to delete
		self.manageRedirectId = $(this).attr("data-id");
		// If we need to delete a row from datatables, it requires a DOM element and not a jQuery object
		self.$manageRedirectRow = $(this).closest('tr')[0];
		// get the correct datatable to remove from
		self.oTable = $("#DataTables_Table_0").dataTable();
		// select the row that will be removed on success
		self.rowIndex = self.oTable.fnGetPosition(self.$manageRedirectRow);
      	$("#DeleteModal").reveal();
	});
	// Confirm Delete Content
	$("#ConfirmRedirectDelete").click(function() {
		var id = self.manageRedirectId;
		$.ajax({
			url: "/Admin/DeleteRedirect",
			type: "POST",
			data: {
				id: id
			},
			success: function(data) {
				var noty_id = noty({ text: 'Redirect Successfully Deleted.', type: 'success', timeout: 2000 });
				self.oTable.fnDeleteRow(self.rowIndex);
				$('#DeleteModal').trigger('reveal:close');
			},
			error: function(data) {
				$('#DeleteModal').trigger('reveal:close');
				var noty_id = noty({ text: 'There was an error processing your request.', type: 'error' });
			}
		});
	});
};
// Keep at the bottom
$(document).ready(function () {
	redirect = new redirect_class();
	redirect.initRedirectEvents();
});

Admin Controller

Add to your admin controller these actions

	[PermissionsFilter(Permissions = "Can Edit Settings")]
	public ActionResult Redirects()
	{
		var model = new RedirectViewModel();
		return View(model);
	}
	[PermissionsFilter(Permissions = "Can Edit Settings")]
	[AcceptVerbs(HttpVerbs.Post)]
	public JsonResult AddRedirect()
	{
		var result = new JsonResult();
		using (var context = new DataContext())
		{
			var redirect = new Redirect { DateModified = DateTime.Now, Active = true };
			context.Redirects.Add(redirect);
			context.SaveChanges();
			var pages = context.ContentPages.ToList();
			var allPages = new List<string>();
			foreach (var page in pages)
			{
				allPages.Add(Utils.NavigationUtils.GetGeneratedUrl(page));
			}
			var options = "<option value=\"/\">/</option>";
			foreach (var page in allPages)
			{
				options += "<option value=\"/content" + page + "\">" + page + "</option>";
			}
			result.Data = new
			{
				source = String.Format(
					"<input class=\"saveField\" type=\"hidden\" data-field=\"RedirectId\" value=\"{0}\" />" +
					"<input class=\"saveField\" type=\"text\" data-field=\"Source\" value=\"\" />", redirect.RedirectId),
				destination =
"<select class=\"saveField\" data-field=\"Destination\" >" +
				options +
				"</select>",
				action = String.Format("<a class=\"delete button secondary small tabletMarginBottom\" href=\"javascript:void(0);\" data-id=\"{0}\">Delete</a>", redirect.RedirectId)
			};
		}
		return result;
	}
	[HttpPost]
	[PermissionsFilter(Permissions = "Can Edit Settings")]
	[AcceptVerbs(HttpVerbs.Post)]
	public JsonResult DeleteRedirect(string id)
	{
		JsonResult result = new JsonResult();
		if (String.IsNullOrEmpty(id))
		{
			return result;
		}
		int redirectId = Int32.Parse(id);
		using (var context = new DataContext())
		{
			var redirect = context.Redirects.FirstOrDefault(x => x.RedirectId == redirectId);
			context.Redirects.Remove(redirect);
			context.SaveChanges();
		}
		// recycle cache after save
		var redirects = CachedObjects.GetRedirectsList(true);
		return result;
	}

Cached Objects

You will need a cached objects file. Look in a Dirigo Ski project for this file. Redirects require a couple methods in there.

	private static readonly object RedirectsListLock = new object();
	/// <summary>
	/// Return a list of Redirects. These don't change very often, no need to check them every page load.
	/// </summary>
	public static List<Redirect> GetRedirects(bool recycleCache, object oLock)
	{
		const string cacheName = "RedirectsCollection"; // Must be unique
		var redirects = (List<Redirect>)HttpRuntime.Cache[cacheName];
		if (redirects == null || recycleCache)
		{
			// Lock cache so simultaneous requests don't also perform this query
			lock (oLock)
			{
				// If we were locked out, check the list again (double-checked locking - http://en.wikipedia.org/wiki/Double-checked_locking)
				redirects = (List<Redirect>)HttpRuntime.Cache[cacheName];
				if (redirects != null && !recycleCache && redirects.Count != 0)
				{
					return redirects;
				}
				using (var context = new DataContext())
				{
					// Otherwise get to caching the data.
					redirects = context.Redirects.ToList();
				}
				HttpRuntime.Cache.Insert(cacheName, redirects, null, DateTime.Now.AddYears(1), Cache.NoSlidingExpiration);
			}
		}
		return redirects;
	}
	public static List<Redirect> GetRedirectsList(bool recycleCache)
	{
		return GetRedirects(recycleCache, RedirectsListLock);
	}

Style

The only thing on the view that needs styling is the add button

	#NewRedirectPage
	{
		position:absolute;
		right:15px;
		top:10px;
	}

Check for redirects before request is complete

Edit your global.asax.cs file to look for redirects. Add to the Application_BeginRequest method.

	// May need to store host in distributed applications
	protected void Application_BeginRequest()
	{
		//string host = Request.Url.Host;
		using (var context = new DataContext())
		{
			// Find out if the redirect exists for this request path
			var redirect = CachedObjects.GetRedirectsList(false).FirstOrDefault(x => x.Source == Request.Path);
			// Check whether the browser remains connected to the server.
			if (Response.IsClientConnected)
			{
				if (redirect != null)
				{
					// If still connected, redirect to another page.
					Response.Redirect(redirect.Destination, false);
				}
			}
			else
			{
				// If the browser is not connected stop all response processing.
				Response.End();
			}
		}
	}

Admin Menu and Scripts

Add this to your settings menu in the admin layout:

	<li>
		<a href="@Url.Action("Redirects", "Admin")">
			<span>Redirects</span>
		</a>
	</li>

You will need to reference the redirects javascript class as well.

	.Add("~/Areas/Admin/Scripts/redirectAdmin.js")

You could just add this on the view, but this way we are still minifying the script.

I'm not sure that many of you can follow along, but, I think that this is a pretty cool solution. This is the sort of coding that we do all day long here at Dirigo.

Thanks!

Thank you for contacting us!

We'll be in touch!

Back Home ×