Francis Rodgers Francis Rodgers - 2 months ago 13
C# Question

C# MVC CMS - Customising Remote Validation

At the link below I asked a question about how to ensure a field does not already contain the same value (for example when there is a unique constraint on a field which correctly causes C# to throw an exception when voilated). With the answer I received, it solved that problem but presented another.

Ensuring a field does not already contain the same value

The main issue I now have is that when I create a new View. The validation works as expected. In brief - The system needs to check that the ViewName and ViewPath (route) are both unique so a search of the DB is required.

However, when I edit the view, the validation kicks in again (and it actually should not because obviously the view exists already because you are editing it).

My issue now is how do I customise the remote validation to work differently for edit vs create. While we should not be able to edit the name of a view to match an existing view, we should also not be stopped from saving the current view simply because it is the same as the current view.

Below is my Model (the part that is not (hopefully) generated by a tool :-):

[MetadataType(typeof(IViewMetaData))]
public partial class View : IViewMetaData { }

public interface IViewMetaData
{
[Required(AllowEmptyStrings = false, ErrorMessageResourceType = typeof(DALResources), ErrorMessageResourceName = "ErrorRequiredField")]
[StringLength(50, ErrorMessageResourceType = typeof(DALResources), ErrorMessageResourceName = "ErrorLessThanCharacters")]
[Display(ResourceType = typeof(DALResources), Name = "ViewName")]
[Remote("IsViewNameAvailable", "Validation")]
string ViewName { get; set; }

[Required(AllowEmptyStrings = false, ErrorMessageResourceType = typeof(DALResources), ErrorMessageResourceName = "ErrorRequiredField")]
[StringLength(400, ErrorMessageResourceType = typeof(DALResources), ErrorMessageResourceName = "ErrorLessThanCharacters")]
[Display(ResourceType = typeof(DALResources), Name = "ViewPath")]
[Remote("IsViewPathAvailable", "Validation")]
string ViewPath { get; set; }

[Display(ResourceType = typeof(DALResources), Name = "ViewContent")]
string ViewContent { get; set; }
}


The part I am having a problem with is the [Remote] validation attribute which is defined below:

[OutputCache(Location = OutputCacheLocation.None, NoStore = true)]
public class ValidationController : Controller
{
private FRCMSV1Entities db = new FRCMSV1Entities();

public JsonResult IsViewNameAvailable(View view)
{
bool isViewNameInvalid = db.View.Any(v => v.ViewName == view.ViewName && v.Id != view.Id);

if (!isViewNameInvalid)
return Json(true, JsonRequestBehavior.AllowGet);

string suggestedViewName = string.Format(UI_Prototype_MVC_Resources.ErrorViewAlreadyExists, view.ViewName);

for (int i = 1; i < 100; i++)
{
string altViewName = view.ViewName + i.ToString();
bool doesAltViewNameExist = db.View.Any(v => v.ViewName == altViewName);
if (!doesAltViewNameExist)
{
suggestedViewName = string.Format(UI_Prototype_MVC_Resources.ErrorViewNotAvailableTry, view.ViewName, altViewName);
break;
}
}
return Json(suggestedViewName, JsonRequestBehavior.AllowGet);
}

public JsonResult IsViewPathAvailable(View view)
{
bool doesViewPathExist = db.View.Any(v => v.ViewPath == view.ViewPath && v.Id != view.Id);

if (!doesViewPathExist)
return Json(true, JsonRequestBehavior.AllowGet);

string suggestedViewPath = string.Format(UI_Prototype_MVC_Resources.ErrorViewAlreadyExists, view.ViewPath);

for (int i = 1; i < 100; i++)
{
string altViewPath = view.ViewPath + i.ToString();
bool doesAltViewPathExist = db.View.Any(v => v.ViewPath == altViewPath);
if (!doesAltViewPathExist)
{
suggestedViewPath = string.Format(UI_Prototype_MVC_Resources.ErrorViewNotAvailableTry, view.ViewPath, altViewPath);
break;
}
}
return Json(suggestedViewPath, JsonRequestBehavior.AllowGet);
}
}


The problem is, the validation needs to work the same on both create and edit. It just needs to do an additional check on edit to ensure we are still referring to the same record and if so, then there is no need to show the validation message because there is nothing wrong.

My question is:
1. How do I get this to work as expected.
2. I can see that both methods are pretty much identical, which violates the DRY principle. How can I make this more generic and simplify it. However the first question is really the one I would like answered because there is no point in refactoring something that doesn't work.

For more information, the above code is also an edit of the code at the following link:

https://msdn.microsoft.com/en-us/library/gg508808(VS.98).aspx

Thanks for any help.

Answer

You need to add a parameter to pass the ID property of the model as AdditionalFields. Assuming its int Id, then

[Remote("IsViewPathAvailable", "Validation", AdditionalFields = "Id")]
public string ViewName { get; set; }

and the the method should be

public JsonResult IsViewNameAvailable(string viewName, int? id)

Note that in the Edit view, you include a hidden input for the Id property, so its value will be posted back by the jquery.validate remote function.

You can then check if the id parameter is null (i.e. it's new) or has a value (it's existing) and adjust the queries to suit.

bool isViewNameInvalid;
if (id.HasValue)
{
    isViewNameInvalid = db.View.Any(v => v.ViewName == viewName && v.Id != id);
}
else
{
    isViewNameInvalid = db.View.Any(v => v.ViewName == ViewName);
}

What is currently happening is that the Remote is only posting the value of the ViewName property, and because your parameter is the model, it is initialized with the default id value (0) and your query is translated to Any(v => v.ViewName == viewName && v.Id != 0);

I also recommend using a view model rather that your partial class

Side note: from the code that generates suggestedViewName, your expecting a lot of ViewName with the same value, meaning your possibly making numerous database calls inside you for loop. You could consider using linq .StartsWith() query to get all the records that start with your ViewName value, and then check the in-memory set in your loop.

Comments