ASP.NET MVC 3 - Custom SEO friendly routes - asp.net-mvc-2

I've defined the following route:
routes.MapRoute(
null,
"foo/{id}/{title}",
new { controller = "Boo", action = "Details" }
);
When I call this method:
Url.Action("Details", "Boo", new { id = article.Id, title = article.Title })
I get the following URL:
http://localhost:57553/foo/1/Some%20text%20Š
I would like to create a new route that will lowercase all characters and replace some of them.
e.g.
http://localhost:57553/foo/1/some-text-s
Rules:
Uppercase -> lowercase
' ' -> '-'
'Š' -> 's'
etc.
Any help would be greatly appreciated!

Seems like a perfect candidate for a custom route:
public class MyRoute : Route
{
public MyRoute(string url, object defaultValues)
: base(url, new RouteValueDictionary(defaultValues), new MvcRouteHandler())
{
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
values = new RouteValueDictionary(values);
var title = values["title"] as string;
if (!string.IsNullOrEmpty(title))
{
values["title"] = SEOify(title);
}
return base.GetVirtualPath(requestContext, values);
}
private string SEOify(string title)
{
throw new NotImplementedException();
}
}
which will be registered like this:
routes.Add(
"myRoute",
new MyRoute(
"foo/{id}/{title}",
new { controller = "Boo", action = "Details" }
)
);
Now all you have to do is to implement your SEO requirements in the SEOify function that I left. By the way you could get some inspiration from the way StackOverflow does it for the question titles.

Related

Response.RedirectToRoute(RouteData.Values) redirects to the Area controller

I am trying to setup a BaseController to handle culture as part of the url (based on ASP.NET MVC 5 Internationalization). My implementation works properly as long as I disable my Areas' registration.
When One of my Area is registered, if I try to input a wrong/not supported culture (http://localhost:52639/zz/), I experience a 404 error with a request URL: http://localhost:52639/fr/Test/Post.
I have checked my routes are properly registered.
If I do the same while disabling the Areas registration, the base controller and routing behave correctly if I type the following URL: http://localhost:52639/zz/ I am redirected to http://localhost:52639/fr/ (default culture).
Those are my routes:
public static void RegisterRoutes(RouteCollection routes)
{
var namespaces = new[]{typeof(PostController).Namespace};
routes.IgnoreRoute("favicon.ico");
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute("PostToHack", "{culture}/Post/{idAndSlug}", new { culture = "", Controller = "Post", Action = "Show" }, namespaces);
routes.MapRoute("Post", "{culture}/Post/{id}-{slug}", new { culture = "", Controller = "Post", Action = "Show" }, namespaces);
routes.MapRoute("TagToHack", "{culture}/Tag/{idAndSlug}", new { culture = "", Controller = "Post", Action = "Tag" }, namespaces);
routes.MapRoute("Tag", "{culture}/Tag/{id}-{slug}", new { culture = "", Controller = "Post", Action = "Tag" }, namespaces);
routes.MapRoute("Logout", "{culture}/Logout", new { culture = "", Controller = "Authentication", Action = "Logout" }, namespaces);
routes.MapRoute("Login", "{culture}/Login", new { culture = "", Controller = "Authentication", Action = "Login" }, namespaces);
//Error routes
routes.MapRoute("Error404", "{culture}/errors/404", new { culture = "", Controller = "Errors", Action = "NotFound" }, namespaces);
routes.MapRoute("Error500", "{culture}/errors/500", new { culture = "", Controller = "Errors", Action = "Error" }, namespaces);
routes.MapRoute("Home", "{culture}", new { culture = "", Controller = "Post", Action = "Index"},namespaces);
//Never to be called by user which is why it comes after MapRoute Home so it is always overwritten by it
routes.MapRoute("Sidebar", "{culture}", new { culture = "", Controller = "Layout", Action = "Sidebar"},namespaces);//This is a "child-only" controller
routes.MapRoute("NavigationBar", "{culture}", new { culture = "", Controller = "Layout", Action = "NavigationBar"},namespaces);//This is a "child-only" controller
Area Route
public override void RegisterArea(AreaRegistrationContext context)
{
var namespaces = new[] { typeof(PostsController).Namespace };
context.MapRoute(
"admin_default",
"{culture}/admin/{controller}/{action}/{id}",
new { culture = "", action = "Index", id = UrlParameter.Optional }, namespaces
);
}
Base Controller:
public class BaseController : Controller
{
protected override IAsyncResult BeginExecuteCore(AsyncCallback callback, object state)
{
var cultureName = RouteData.Values["culture"] as string;
// Attempt to read the culture cookie from Request
if (cultureName == null)
cultureName = (Request.UserLanguages != null) && (Request.UserLanguages.Length > 0)
? Request.UserLanguages[0]
: null; // obtain it from HTTP header AcceptLanguages
// Validate culture name
cultureName = CultureHelper.GetImplementedCulture(cultureName); // This is safe
if (RouteData.Values["culture"] as string != cultureName)
{
// Force a valid culture in the URL
RouteData.Values["culture"] = cultureName.ToLowerInvariant(); // lower case too
// Redirect user
Response.RedirectToRoute(RouteData.Values);
}
// Modify current thread's cultures
Thread.CurrentThread.CurrentCulture = new CultureInfo(cultureName);
Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture;
return base.BeginExecuteCore(callback, state);
}
}
After some more digging I have found a solution that work for me. My issue was coming from the order in which I was registering my routes. I was registering my Area's routes first:
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RouteConfig.RegisterRoutes(RouteTable.Routes);
}
I inverted the order and made sure that I was only registering my Area's routes after:
protected void Application_Start()
{
RouteConfig.RegisterRoutes(RouteTable.Routes);
AreaRegistration.RegisterAllAreas();
}

Orchard work flow - Send email to multiple admin and moderator on content post/Update

I am working with Orchard CMS.
I have created work flow which send mail to Super admin and user himself who create block/news as a notification.
In my CMS there more than one admin and moderator. I want to send email to all moderator and admin as soon as new blog /news/article is posted or updated. Right now it is sending to only one admin user not all.
How can I create workflow to send mail to all admin as well as moderator once content (news/blog) is posted or updated?
I am working on orchard version 1.10.0.
There are two possibilities here: A custom workflow task and a simple token.
Let's start with the task. I've created a simple demo; here's the code:
Activities/RoleMailerTask.cs
namespace My.Module
{
using System.Collections.Generic;
using System.Linq;
using Orchard;
using Orchard.ContentManagement;
using Orchard.Data;
using Orchard.Email.Activities;
using Orchard.Email.Services;
using Orchard.Environment.Extensions;
using Orchard.Localization;
using Orchard.Messaging.Services;
using Orchard.Roles.Models;
using Orchard.Roles.Services;
using Orchard.Users.Models;
using Orchard.Workflows.Models;
using Orchard.Workflows.Services;
[OrchardFeature(Statics.FeatureNames.RoleMailerTask)]
public class RoleMailerTask : Task
{
private readonly IOrchardServices services;
public const string TaskName = "RoleMailer";
public RoleMailerTask(IOrchardServices services)
{
this.services = services;
this.T = NullLocalizer.Instance;
}
public Localizer T { get; set; }
public override string Name
{
get { return TaskName; }
}
public override string Form
{
get { return TaskName; }
}
public override LocalizedString Category
{
get { return this.T("Messaging"); }
}
public override LocalizedString Description
{
get { return this.T("Sends mails to all users with this chosen role"); }
}
public override IEnumerable<LocalizedString> GetPossibleOutcomes(WorkflowContext workflowContext, ActivityContext activityContext)
{
yield return this.T("Done");
}
public override IEnumerable<LocalizedString> Execute(WorkflowContext workflowContext, ActivityContext activityContext)
{
var recipientsRole = int.Parse(activityContext.GetState<string>("RecipientsRole"));
var role = this.services.WorkContext.Resolve<IRoleService>().GetRole(recipientsRole);
var userRolesRepo = this.services.WorkContext.Resolve<IRepository<UserRolesPartRecord>>();
var recepientIds = userRolesRepo.Table.Where(x => x.Role.Name == role.Name).Select(x => x.UserId);
var recipients = this.services.ContentManager.GetMany<UserPart>(recepientIds, VersionOptions.Published, QueryHints.Empty)
.Where(x => !string.IsNullOrEmpty(x.Email))
.Select(x => x.Email);
var body = activityContext.GetState<string>("Body");
var subject = activityContext.GetState<string>("Subject");
var replyTo = activityContext.GetState<string>("ReplyTo");
var bcc = activityContext.GetState<string>("Bcc");
var cc = activityContext.GetState<string>("CC");
var parameters = new Dictionary<string, object> {
{"Subject", subject},
{"Body", body},
{"Recipients", string.Join(",", recipients)},
{"ReplyTo", replyTo},
{"Bcc", bcc},
{"CC", cc}
};
var queued = activityContext.GetState<bool>("Queued");
if ( !queued )
{
this.services.WorkContext.Resolve<IMessageService>().Send(SmtpMessageChannel.MessageType, parameters);
}
else
{
var priority = activityContext.GetState<int>("Priority");
this.services.WorkContext.Resolve<IJobsQueueService>().Enqueue("IMessageService.Send", new { type = SmtpMessageChannel.MessageType, parameters = parameters }, priority);
}
yield return T("Done");
}
}
}
You might want to throw the recipients in a loop, and send a new email for each of them. With the version above, everyone will see the mail address of everyone else...
Forms/RoleMailerTaskForm.cs
namespace My.Module
{
using System;
using System.Linq;
using System.Web.Mvc;
using Activities;
using Orchard;
using Orchard.ContentManagement;
using Orchard.DisplayManagement;
using Orchard.Environment.Extensions;
using Orchard.Forms.Services;
using Orchard.Roles.Services;
[OrchardFeature(Statics.FeatureNames.RoleMailerTask)]
public class RoleMailerTaskForm : Component, IFormProvider
{
private readonly IRoleService roleService;
public RoleMailerTaskForm(IShapeFactory shapeFactory, IRoleService roleService)
{
this.roleService = roleService;
this.Shape = shapeFactory;
}
public dynamic Shape { get; set; }
public void Describe(DescribeContext context)
{
Func<IShapeFactory, dynamic> formFactory = shape =>
{
var form = this.Shape.Form(
Id: RoleMailerTask.TaskName,
_Type: this.Shape.FieldSet(
Title: this.T("Send to"),
_RecepientsRole: this.Shape.SelectList(
Id: "recipientsRole",
Name: "RecipientsRole",
Title: this.T("Recepient role"),
Description: this.T( "The role of the users that should be notified" ),
Items: this.roleService.GetRoles().Select(r => new SelectListItem { Value = r.Id.ToString(), Text = r.Name })
),
_Bcc: this.Shape.TextBox(
Id: "bcc",
Name: "Bcc",
Title: this.T("Bcc"),
Description: this.T("Specify a comma-separated list of email addresses for a blind carbon copy"),
Classes: new[] { "large", "text", "tokenized" }),
_CC: this.Shape.TextBox(
Id: "cc",
Name: "CC",
Title: this.T("CC"),
Description: this.T("Specify a comma-separated list of email addresses for a carbon copy"),
Classes: new[] { "large", "text", "tokenized" }),
_ReplyTo: this.Shape.Textbox(
Id: "reply-to",
Name: "ReplyTo",
Title: this.T("Reply To Address"),
Description: this.T("If necessary, specify an email address for replies."),
Classes: new[] { "large", "text", "tokenized" }),
_Subject: this.Shape.Textbox(
Id: "Subject", Name: "Subject",
Title: this.T("Subject"),
Description: this.T("The subject of the email message."),
Classes: new[] { "large", "text", "tokenized" }),
_Message: this.Shape.Textarea(
Id: "Body", Name: "Body",
Title: this.T("Body"),
Description: this.T("The body of the email message."),
Classes: new[] { "tokenized" })
));
return form;
};
context.Form(RoleMailerTask.TaskName, formFactory);
}
}
This is the form you'll need to configure the task. The form is a blatant rip off from Orchard.Email/Forms/EmailForm. The only thing I've changed is the select list on top of the form.
And that's all you need!
For the token approach, you would just need to define a token like
{RoleRecpients:TheRoleYouWant}, parse that role name and get the users like shown in the Task above.

Mocking controller UrlHelper

I'm having problems retrieving the route url from controller.RouteUrl(routeName) method.
Here is my code for mocking the urls in my test method:
//Arrange
...
//Mock routes
var routes = RouteTable.Routes;
routes.Clear();
routes.MapRoute(
"AdminPaymentResult", // Route name
"Payment/Result"); // URL with parameters
routes.MapRoute(
"AdminPaymentCancel", // Route name
"Payment/Cancel"); // URL with parameters
_controller.SetFakeUrlHelper(routes);
...
where the method SetFakeUrlHelper is defined as:
public static void SetFakeUrlHelper(this Controller controller, RouteCollection routes)
{
var fakeHttpContext = FakeHttpContext();
var requestContext = new RequestContext(fakeHttpContext, new RouteData());
controller.Url = new UrlHelper(requestContext, routes);
}
and the method FakeHttpContext is defined as:
public static HttpContextBase FakeHttpContext()
{
var request = new Mock<HttpRequestBase>();
var response = new Mock<HttpResponseBase>();
var session = new Mock<HttpSessionStateBase>();
var server = new Mock<HttpServerUtilityBase>();
var context = new Mock<HttpContextBase>();
context.Setup(ctx => ctx.Request).Returns(request.Object);
context.Setup(ctx => ctx.Response).Returns(response.Object);
context.Setup(ctx => ctx.Session).Returns(session.Object);
context.Setup(ctx => ctx.Server).Returns(server.Object);
return context.Object;
}
The problem is that when in my controller action I call
public ActionResult MyAction()
{
...
var callBackUrl = Url.RouteUrl("AdminPaymentResult");
...
}
I get an empty string instead of "Payment/Result" as expected...
Thanks in advance
You should mock the ApplyAppPathModifier method on the response which is internally used by the UrlHelper. So just add the following line inside your FakeHttpContext method and you will be good to go:
response
.Setup(x => x.ApplyAppPathModifier(It.IsAny<string>()))
.Returns<string>(x => x);

Refresh a ListBox in ASP.NET MVC 2 with jQuery

I've looked around on SO to see if this has been asked before and couldn't find anything (so if it has indeed been asked before then I apologize.
Here's what I'm trying to do, a user can select from a list of skills for their profile, if a skill they want isn't in the list then they can add it to the database, I have that accomplished with WCF & jQuery AJAX. Here's the code for that:
$("#AddNewSkill").click(function () {
$("#AddNewSkill").attr("disabled", true);
$("#newSkill").attr("disabled", true);
var newSkill = $("#newSkill").val();
var data = { name: $("#newSkill").val(), description: "", type: "Skill" };
data = JSON.stringify(data)
$.ajax({
type: "POST",
contentType: "application/json; charset=utf-8",
url: "../WeddingPhotographerService.svc/AddNew",
data: data,
dataType: "json",
success: function () {
successCall('#newSkill', '#AddNewSkill');
//$('#SkillListViewContainer').load('../Account/GetSkillControl');
},
error: function (msg) {
$("#AddSkillError").text(msg.d);
$("#AddSkill").attr("disabled", false);
$("#NewSkill").attr("disabled", false);
}
});
});
Here's the method in the AJAX-Enabled WCF service:
[OperationContract]
public bool AddNew(string name, string description, string type)
{
switch (type)
{
case "":
goto default;
case "skill":
IRepository<Skill> skillRepo = ObjectFactory.GetInstance<IRepository<Skill>>();
var skill = new Skill { Name = name, Description = (string.IsNullOrEmpty(description)) ? string.Empty : description };
skillRepo.Save(skill);
return true;
case "equipment":
IRepository<Equipment> eqRep = ObjectFactory.GetInstance<IRepository<Equipment>>();
var eq = new Equipment { Name = name, Description = (string.IsNullOrEmpty(description)) ? string.Empty : description };
eqRep.Save(eq);
return true;
case "occasion":
IRepository<Occassion> occRepo = ObjectFactory.GetInstance<IRepository<Occassion>>();
var oc = new Occassion { Name = name, Description = (string.IsNullOrEmpty(description)) ? string.Empty : description };
occRepo.Save(oc);
return true;
default:
IRepository<Skill> repo = ObjectFactory.GetInstance<IRepository<Skill>>();
var s = new Skill { Name = name, Description = (string.IsNullOrEmpty(description)) ? string.Empty : description };
repo.Save(s);
return true;
}
}
It's kind of ugly but I'll optimize it once I have this 2nd part working. Here's how the ListBox is being loaded in the view:
<%: Html.ListBox("Skills", Model.SkillList, new { #style = "width:157px; height:90px;background:#e2f0f1;", #size = "3", #class = "inputbox" })%>
Which comes from RegistrationModelView.cs, here's SkillList in my model view:
private MultiSelectList GetSkills(string[] selectedValues)
{
List<Skill> list = new List<Skill>();
IRepository<Skill> skills = ObjectFactory.GetInstance<IRepository<Skill>>();
foreach (Skill skill in skills.GetAll())
{
list.Add(new Skill()
{
Key = skill.Key,
Name = skill.Name,
Description = ""
});
}
return new MultiSelectList(list, "Key", "Name", selectedValues);
}
And the action in AccountController.cs that loads the view
[MoveFormsScript]
public ActionResult Index()
{
return View(new RegistrationModelView());
}
I'm pretty sure all the code I posted (other than how the new item is added with the WCF service and the jQuery for consuming said service is irrelevant but I thought I'd offer as much information as possible).
Like I said the new value is added to the database no problem, my issue is updating the ListBox to reflect the new values. Anyone got any ideas and can help with this?
Well I mucked around until I found something that does what I need it to do. It may not be the most efficient or elegant way to accomplish the task but it at least works (Maybe someone will come along with a different solution some day).
What I ended up doing was make another $,ajax() call in the success of the first call like this
$("#AddNewSkill").click(function () {
$("#AddNewSkill").attr("disabled", true);
$("#newSkill").attr("disabled", true);
var data = { name: $("#newSkill").val(), description: "", type: "skill" };
data = JSON.stringify(data)
$.ajax({
type: "POST",
contentType: "application/json; charset=utf-8",
url: "../WeddingPhotographerService.svc/AddNew",
data: data,
dataType: "json",
success: function () {
$.ajax({
type:"POST",
datatype:"json",
url: "../Account/GetSkills",
success:updateSkillsListBox
});
},
error: function (msg) {
alert(msg.d);
}
});
});
function updateSkillsListBox(data, status) {
$("#Skills").html("");
for(var d in data) {
$("<option value=\"" + data[d].Value + "\">" + data[d].Name + "</option>").appendTo("#Skills");
}

Querystring Route in MVC2

I'm trying to create a route to a specific controller/action which needs to accept optional querystring parameters.
the urls i'd like to accept are:
/Products/ProductsListJson
/Products/ProductsListJson?productTypeId=1
/Products/ProductsListJson?productTypeId=1&brandId=2
/Products/ProductsListJson?productTypeId=1&brandId=2&year=2010
I have an action like this:
public JsonResult ProductsListJson(int productTypeId, int brandId, int year)
And a route like this:
routes.MapRoute(
null, "Products/ProductsListJson",
new { controller = "Products", action = "ProductsListJson", productTypeId = 0, brandId = 0, year = 0 }
);
I assumed that the action "ProductsListJson" would simply see the querystring urls and map them to the appropriate arguments however this is not happening.
Anyone know how this could be achived?
You don't need to specify their values in the route if those parameters are passed in the query string:
routes.MapRoute(
null, "Products/ProductsListJson",
new { controller = "Products", action = "ProductsListJson" }
);
and your action:
public ActionResult ProductsListJson(int? productTypeId, int? brandId, int? year)
{
...
}
but you probably don't need a specific route for this as the default route will handle it just fine:
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);