What would be equivalent of IModelBinderProvider in WEBAPI?
I have read this article by still not understand how to replace default binding or insert its own binding rules.
EDIT: Current 'classic' MVC model binder provider (implements IModelBinderProvider).
public IModelBinder GetBinder(Type modelType)
{
Type binderType;
lock (_syncLock)
{
// Check to see if a type was already bound.
if (_binders.ContainsKey(modelType))
binderType = _binders[modelType];
else
{
// Check the assembly and look for a <name>ModelBinder
Assembly assembly = Assembly.GetExecutingAssembly();
Type[] types = assembly.GetTypes();
// Convention over configuration
binderType = types.FirstOrDefault(t => t.Name == modelType.Name + "ModelBinder");
if (binderType != null)
_binders[modelType] = binderType;
}
}
if (binderType == null)
return null;
var binder = (IModelBinder) DependencyResolver.Current.GetService(binderType);
return binder;
}
So you can see I am trying to use dependency resolver for binder selection.
Also in configuration I replaced default binder with
DependencyResolver.SetResolver(new StructureMapDependencyResolver(ObjectFactory.Container));
At the end if I send parameter (as form or querystring) in example "LocationID" and I have webapi action like this
public HttpResponseMessage MyAction(MyCls obj) {
// do something
}
public class MyCls{
public string LocationID {get;set;}
}
I need MyCls to be bound and LocationID to get value of provided "LocationID" param.
Related
I have this very weird problem that I cannot get my head around. Perhaps someone could point out what I am doing wrong.
Basically, I am just trying to search items using Linq to Sitecore.
So, my classes look like ( I am using glass too)
[SitecoreType(TemplateId = "{TEMPLATE_GIUD}")]
public class MyMappedClass : SharedFieldClass
{
[SitecoreField(FieldName = "mylist")]
public virtual IEnumerable<SharedFieldClass> MyMultilistField { get; set; }
[SitecoreField(FieldName = "field1")]
[IndexField("field1")]
public virtual MyKeyValue field1 { get; set; }
}
[SitecoreType]
public class MyKeyValue
{
public virtual Sitecore.Data.ID Id {get;set;}
public virtual string MyValue{get;set;}
}
So when I do the below query it works as it's supposed to.
var results = context.GetQueryable<SearchResultItem>()
.Where(c => ((string)c["field1"]) == "{GUID}").GetResults();
But, when I do the below it returns 0 result.
List<MyMappedClass> results = context.GetQueryable<MyMappedClass>()
.Where(c => c.field1.MyValue == "{GUID}").ToList();
I have read this link . And I have followed the 2nd process described here for Glass to work with Sitecore7 Search (the "SharedFieldClass" contains all the basic index fields).
This is a pretty obvious scenario and I'm sure lots of people have done it already and I am doing something silly here.
Thanks in advance.
/## EDIT ##/
Okay so I've done a bit more digging on this. Not sure if it's a bug in ContentSearch/Luncene.NET API or I am missing something BUT seems like what was posted here is probably not TRUE and if you have a complex field type ( which you will ) you can not query with a mapped class against Lucene. ( not sure if this is also the case for Solr). If you are doing search against simple type like string and int then it works like a charm.
SO here're my findings:
After enabling DEBUG and LOG on for contentsearch I found that if I query like this context.GetQueryable<MyMappedClass>().Where(c => c.field1.MyValue == "{GUID}") what it gets translated into is DEBUG Executing lucene query: field1.value:7e9ed2ae07194d83872f9836715eca8e and as there's no such thing in the index named "field1.value" the query doesn't return anything. The name of the index is actually just "field1". Is this a bug ??
However, query like this context.GetQueryable<SearchResultItem>() .Where(c => ((string)c["field1"]) == "{GUID}").GetResults(); works because it gets translated into "DEBUG Executing lucene query: +field1:7e9ed2ae07194d83872f9836715eca8e".
I also did write a method in my mapped class like below:
public string this[string key]
{
get
{
return key.ToLowerInvariant();
}
set { }
}
Which allowed me write the below query with my MyMappedClass.
results2 = context.GetQueryable<MyMappedClass>().Where(c => c["filed1"]== "{GUID}").ToList();
This returned expected result. BUT the values of the fields in MyMappedClass are not filled ( in fact they're all null except the core/shared values like templateid, url etc which are populated in the filled document). So the result list are pretty much useless. I could do a loop over all of them and manually get the values populated as I have the itemid. But imagine the cost for a large result set.
Lastly I did this:
<fieldType fieldTypeName="droplink" storageType="YES" indexType="TOKENIZED" vectorType="NO" boost="1f" type="System.String" settingType="Sitecore.ContentSearch.LuceneProvider.LuceneSearchFieldConfiguration, Sitecore.ContentSearch.LuceneProvider" />
So this returned populated "field1" with the itemid in lucene query using "IndexViewer2.0". BUT this fails for MyMappedClass too as the value of "field1" in the document is a System.string .. but it is mapped as "MyKeyValue" in MyMappedClass SO it throws the below exception
Exception: System.InvalidCastException
Message: Invalid cast from 'System.String' to 'MyLib.MyKeyValue'.
SO, the big question is:
How does one query using his/her mapped class using the cool ContentSearch API ?
I bit more further digging got me to a working solution. Posting it here just in case anyone runs into this issue.
This is how my "SharedFieldClass" looked like ( which was somewhat wrong )
public abstract class SharedFieldClass
{
[SitecoreId]
[IndexField("_id")]
[TypeConverter(typeof(IndexFieldIDValueConverter))]
public virtual ID Id { get; set; }
[SitecoreInfo(SitecoreInfoType.Language)]
[IndexField("_language")]
public virtual string Language { get; set; }
[SitecoreInfo(SitecoreInfoType.Version)]
public virtual int Version
{
get
{
return Uri == null ? 0 : Uri.Version.Number;
}
}
[TypeConverter(typeof(IndexFieldItemUriValueConverter))]
[XmlIgnore]
[IndexField("_uniqueid")]
public virtual ItemUri Uri { get; set; }
}
And there's a class in Glass that does the mapping. Which looks like below:
var sitecoreService = new SitecoreService("web");
foreach (var r in results)
{
sitecoreService.Map(r);
}
for me this class was failing to map because of this line:
[SitecoreId]
[IndexField("_id")]
[TypeConverter(typeof(IndexFieldIDValueConverter))]
public virtual ID Id { get; set; }
It was throwing a NULL exception at sitecoreService.Map(r); line
So I changed it to below:
[SitecoreId]
[IndexField("_group")]
[TypeConverter(typeof(IndexFieldIDValueConverter))]
public virtual ID Id { get; set; }
And it worked. I'm not sure when the index field for ItemId in sitecore changed from "_id" to "_group" or whether it was always like that. But it is "_group" in the SearchResultItem class. So I used it and mapping was successful.
So to Sum it all the solution looks like this:
The Mapped Class:
[SitecoreType(TemplateId = "{TEMPLATE_GIUD}")]
public class MyMappedClass : SharedFieldClass
{
[SitecoreField(FieldName = "mylist")]
public virtual IEnumerable<SharedFieldClass> MyMultilistField { get; set; }
[SitecoreField(FieldName = "field1")]
[IndexField("field1")]
public virtual MyKeyValue field1 { get; set; }
// Will be set with key and value for each field in the index document
public string this[string key]
{
get
{
return key.ToLowerInvariant();
}
set { }
}
}
[SitecoreType]
public class MyKeyValue
{
public virtual Sitecore.Data.ID Id {get;set;}
public virtual string MyValue{get;set;}
}
And the query is:
List<MyMappedClass> results = context.GetQueryable<MyMappedClass>()
.Where(c => c["field1"] == "{GUID}").ToList();
That's it.
/* edited */
OH!! Wait that's not it. This will still fail to cast the complex type "MyKeyValue" when the "field1" is populated with guid in the index document.
So to avoid this I had to write my custom converter as #Michael Edwards suggested.
I had to modify the class slightly to fit my needs ..
public class IndexFieldKeyValueModelConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
var config = Glass.Mapper.Context.Default.GetTypeConfiguration<SitecoreTypeConfiguration>(sourceType, true);
if (config != null && sourceType == typeof(MyLib.IKeyValue))
{
return true;
}
else
return base.CanConvertFrom(context, sourceType);
}
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
if (destinationType == typeof(string))
return true;
else
return base.CanConvertTo(context, destinationType);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
var scContext = new SitecoreContext();
Guid x = Guid.Empty;
if(value is string)
{
x = new Guid((string)value);
}
var item = scContext.Database.GetItem(x.ToString());
if (item == null)
return null;
return scContext.CreateType(typeof(MyLib.IKeyValue), item, true, false, new Dictionary<string, object>());
}
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
var config = Glass.Mapper.Context.Default.GetTypeConfiguration<SitecoreTypeConfiguration>(value.GetType(), true);
ID id = config.GetId(value);
return id.ToShortID().ToString().ToLowerInvariant();
}
}
Not sure if it were the expected behaviour or not .. but for some reason, in the convertfrom method the value of the "object value" parameter was short string id format. So for scContext.Database.GetItem to work I had to convert it to a proper GUID and then it started returning proper item rather than null.
AND then I wrote my query like this:
results = context.GetQueryable<MyMappedGlassClass>().Where(c => c["field1"] == field1value && c["field2"] == field2value && c["_template"] == templateId).Filter(selector => selector["_group"] != currentId).ToList();
Looks like a fair bit of work to get it to work. I guess using the LinqHelper.CreateQuery method is the easy way out .. but as Mr. Pope suggested here that this method is not be used as this is an internal method.
Not sure what's the balance here.
/* end edited */
Please see my question description section for explanation on why I had to do things this way.
Also, (I bias opinion ) is the trick described here may not be valid anymore (please see my question description's edit section for the reason behind).
Also, index field for itemid in the Glass Mapper tutorial here is I think wrong (unless otherwise proven).
Hope it helps someone saving/wasting time.
You could create a custom field mapper for lucene that would convert from the Guid in the index to a glass model. I hacked this out but I haven't tested it:
public class IndexFieldDateTimeValueConverter : TypeConverter
{
public Type RequestedType { get; set; }
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
var config = Glass.Mapper.Context.Default.GetTypeConfiguration<SitecoreTypeConfiguration>(sourceType, true);
if (config != null)
{
RequestedType = sourceType;
return true;
}
else
return base.CanConvertFrom(context, sourceType);
}
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
if (destinationType == typeof(string))
return true;
else
return base.CanConvertTo(context, destinationType);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
var scContext = new SitecoreContext();
return scContext.CreateType(RequestedType, scContext.Database.GetItem(value.ToString()),true, false, new Dictionary<string, object>());
}
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
var config = Glass.Mapper.Context.Default.GetTypeConfiguration<SitecoreTypeConfiguration>(value.GetType(), true);
ID id =config.GetId(value);
return id.ToShortID().ToString().ToLowerInvariant();
}
My concern is that the ConvertFrom method does not get passed the type requested so we have to store this as property on the class to pass it from the CanConvertFrom method to the ConvertFrom method. This makes this class not thread safe.
Add this to the indexFieldStorageValueFormatter section of the sitecore config.
The problem here is that SearchResultItem is not actually an Item, but does have the GetItem() method to get the Sitecore item. What you need to do is the following:
List<MyMappedClass> results = context.GetQueryable<SearchResultItem>()
.Select(sri => sri.GetItem())
.Where(i => i != null)
.Select(i => i.GlassCast<MyMappedClass>())
.Where(c => c.field1.MyValue == "{GUID}").ToList();
I haven't worked with Glass specifically, but if you change your parent class to SearchResultItem, does it begin working? If so, that would indicate an issue with the SharedFieldClass parent class.
I have a view model that implements IValidatableObject that contains a string and a collection of another view model, something like this:
public sealed class MainViewModel
{
public string Name { get; set; }
public ICollection<OtherViewModel> Others { get; set; }
}
My validation checks each object in Others against different rules using the contract provided by IValidatableObject:
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
foreach (var other in this.Others)
{
// validate or yield return new ValidationResult
}
}
Because of the complex structure of the real MainViewModel I have had to create a custom model binder which re-builds the model and assigns POST data to the relevant components. The problem that I'm getting is that nothing is getting validated resulting in validation errors at the context level as it violates certain database constraints and I'm not sure what I'm doing wrong - I assumed that ModelState.IsValid would invoke the Validate method on my view model but it doesn't seem to go down that way.
My model binder looks like this:
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
int modelId = (int)controllerContext.RouteData.Values["id"];
// query the database and re-build the components of the view model
// iterate the POST data and assign to the model where necessary
// should I be calling something here to validate the model before it's passed to the controller?
return model;
}
Any help appreciated!
Validator.TryValidateObject
OK, seems I'm a little closer. I can now get my IValidatableObject method to run by adding the following to my custom model binder:
var validationResults = new HashSet<ValidationResult>();
var isValid = Validator.TryValidateObject(model, new ValidationContext(model, null, null), validationResults, true);
Seems that Validator.TryValidateObject invokes the validation method and setting the last parameter to true causes it to validate all properties. However, I'm now stuck with getting the validationResults to the controller so they can be used in a meaningful way.
I should have realised that I could use the ModelState.AddModelError through a custom binder, I've managed to get this working correctly now by adding the following to my custom model binder before returning the model to the controller:
var validationResults = new HashSet<ValidationResult>();
var isValid = Validator.TryValidateObject(model, new ValidationContext(model, null, null), validationResults, true);
if (!isValid)
{
foreach (var result in validationResults)
{
bindingContext.ModelState.AddModelError("", result.ErrorMessage);
}
}
return model;
This now returns a list of all errors to my page and the ModelState.IsValid check on my controller action is now returning false.
Paul's great answer can be refactored into a generic validate-and-convert to ModelState method as follows (e.g. in a helper or CustomModelBinder base). In addition, the bindings to the validated properties are retained.
public static void DoValidation(ModelBindingContext bindingContext,
IValidatableObject model)
{
var validationResults = new HashSet<ValidationResult>();
var isValid = Validator.TryValidateObject(model,
new ValidationContext(model, null, null), validationResults, true);
if (!isValid)
{
var resultsGroupedByMembers = validationResults
.SelectMany(_ => _.MemberNames.Select(
x => new {MemberName = x ?? "",
Error = _.ErrorMessage}))
.GroupBy(_ => _.MemberName);
foreach (var member in resultsGroupedByMembers)
{
bindingContext.ModelState.AddModelError(
member.Key,
string.Join(". ", member.Select(_ => _.Error)));
}
}
}
I'm using SPMetal in order to generate entity classes for my sharepoint site and I'm not exactly sure what the best practice is to use when there are multiple content types for a single list. For instance I have a task list that contains 2 content types and I'm defining them via the config file for SPMetal. Here is my definition...
<List Member="Tasks" Name="Tasks">
<ContentType Class="LegalReview" Name="LegalReviewContent"/>
<ContentType Class="Approval" Name="ApprovalContent"/>
</List>
This seems to work pretty well in that the generated objects do inherit from WorkflowTask but the generated type for the data context is a List of WorkflowTask. So when I do a query I get back a WorkflowTask object instead of a LegalReview or Approval object. How do I make it return an object of the correct type?
[Microsoft.SharePoint.Linq.ListAttribute(Name="Tasks")]
public Microsoft.SharePoint.Linq.EntityList<WorkflowTask> Tasks {
get {
return this.GetList<WorkflowTask>("Tasks");
}
}
UPDATE
Thanks for getting back to me. I'm not sure how I recreate the type based on the SPListItem and would appreciate any feedback.
ContractManagementDataContext context = new ContractManagementDataContext(_url);
WorkflowTask task = context.Tasks.FirstOrDefault(t => t.Id ==5);
Approval a = new Approval(task.item);
public partial class Approval{
public Approval(SPListItem item){
//Set all properties here for workflowtask and approval type?
//Wouldn't there be issues since it isn't attached to the datacontext?
}
public String SomeProperty{
get{ //get from list item};
set{ //set to list item};
}
Linq2SharePoint will always return an object of the first common base ContentType for all the ContentTypes in the list. This is not only because a base type of some description must be used to combine the different ContentTypes in code but also it will then only map the fields that should definitely exist on all ContentTypes in the list. It is however possible to get access to the underlying SPListItem returned by L2SP and thus from that determine the ContentType and down cast the item.
As part of a custom repository layer that is generated from T4 templates we have a partial addition to the Item class generated by SPMetal which implements ICustomMapping to get the data not usually available on the L2SP entities. A simplified version is below which just gets the ContentType and ModifiedDate to show the methodology; though the full class we use also maps Modified By, Created Date/By, Attachments, Version, Path etc, the principle is the same for all.
public partial class Item : ICustomMapping
{
private SPListItem _SPListItem;
public SPListItem SPListItem
{
get { return _SPListItem; }
set { _SPListItem = value; }
}
public string ContentTypeId { get; internal set; }
public DateTime Modified { get; internal set; }
public virtual void MapFrom(object listItem)
{
SPListItem item = (SPListItem)listItem;
this.SPListItem = item;
this.ContentTypeId = item.ContentTypeId.ToString();
this.Modified = (DateTime)item["Modified"];
}
public virtual void MapTo(object listItem)
{
SPListItem item = (SPListItem)listItem;
item["Modified"] = this.Modified == DateTime.MinValue ? this.Modified = DateTime.Now : this.Modified;
}
public virtual void Resolve(RefreshMode mode, object originalListItem, object databaseObject)
{
SPListItem originalItem = (SPListItem)originalListItem;
SPListItem databaseItem = (SPListItem)databaseObject;
DateTime originalModifiedValue = (DateTime)originalItem["Modified"];
DateTime dbModifiedValue = (DateTime)databaseItem["Modified"];
string originalContentTypeIdValue = originalItem.ContentTypeId.ToString();
string dbContentTypeIdValue = databaseItem.ContentTypeId.ToString();
switch(mode)
{
case RefreshMode.OverwriteCurrentValues:
this.Modified = dbModifiedValue;
this.ContentTypeId = dbContentTypeIdValue;
break;
case RefreshMode.KeepCurrentValues:
databaseItem["Modified"] = this.Modified;
break;
case RefreshMode.KeepChanges:
if (this.Modified != originalModifiedValue)
{
databaseItem["Modified"] = this.Modified;
}
else if (this.Modified == originalModifiedValue && this.Modified != dbModifiedValue)
{
this.Modified = dbModifiedValue;
}
if (this.ContentTypeId != originalContentTypeIdValue)
{
throw new InvalidOperationException("You cannot change the ContentTypeId directly");
}
else if (this.ContentTypeId == originalContentTypeIdValue && this.ContentTypeId != dbContentTypeIdValue)
{
this.ContentTypeId = dbContentTypeIdValue;
}
break;
}
}
}
Once you have the ContentType and the underlying SPListItem available on your L2SP entity it is simply a matter of writing a method which returns an instance of the derived ContentType entity from a combination of the values of the base type and the extra data for the missing fields from the SPListItem.
UPDATE: I don't actually have an example converter class as we don't use the above mapping extension to Item in this way. However I could imagine something like this would work:
public static class EntityConverter
{
public static Approval ToApproval(WorkflowTask wft)
{
Approval a = new Approval();
a.SomePropertyOnWorkflowTask = wft.SomePropertyOnWorkflowTask;
a.SomePropertyOnApproval = wft.SPListItem["field-name"];
return a;
}
}
Or you could put a method on a partial instance of WorkflowTask to return an Approval object.
public partial class WorkflowTask
{
public Approval ToApproval()
{
Approval a = new Approval();
a.SomePropertyOnWorkflowTask = this.SomePropertyOnWorkflowTask;
a.SomePropertyOnApproval = this.SPListItem["field-name"];
return a;
}
public LegalReview ToLegalReview()
{
// Create and return LegalReview as for Approval
}
}
In either situation you would need to determine the method to call to get the derived type from the ContentTypeId property of the WorkflowTask. This is the sort of code I would normally want to generate in one form or another as it will be pretty repetitive but that is a bit off-topic.
I'm trying to figure out the valid usages of DisplayAttribute.GroupName property.
MSDN says:
A value that is used to group fields in the UI.
but I wouldn't call it a comprehensive explanation. It makes me think that GroupName can be used to create groupboxes around certain fields. But then the remark:
Do not use this property to get the value of the GroupName property.
Use the GetDescription method instead. A null value or empty string is
valid.
seems to contradict it.
So what is this property for and should I use it (probably with custom template or custom ModelMetadataProvider) in order to render groupboxes around my fields?
In the MVC RTM source code there is no sign of usage.
The "GetDescription" remark might be a copy/paste error in the documentation (each string property seems to have a GetXXX counterpart that returns a localizable value), so it should be most probably "GetGroupName" in this case.
UPDATE:
I would use it exactly for that: group fields together that belong together from the UI point-of-view. As this is just data annotation on the model, it declares only that these fields belong to one logical group "somehow" on the UI, the but concrete presentation details depend on the "UI engine" that displays the model based on the metadata.
I think the most meaningful way to "render" this on the UI is exactly what you said: wrapping the grouped fields into a section or fieldset.
Of course there might be future extensions of MVC or other custom extensions that do some kind of grouping on the UI "automatically" (without writing custom code that examines the metadata and generates the sections) based on this attribute property. But I'm quite sure that such an extension would do something very similar that you would do currently.
I ended up writing this class to make the GroupName more easily accessible:
public class ExtendedDataAnnotationsModelMetadataProvider : DataAnnotationsModelMetadataProvider
{
public const string Key_GroupName = "GroupName";
protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
{
ModelMetadata modelMetadata = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);
DisplayAttribute displayAttribute = attributes.OfType<DisplayAttribute>().FirstOrDefault();
if (displayAttribute != null)
modelMetadata.AdditionalValues[ExtendedDataAnnotationsModelMetadataProvider.Key_GroupName] = displayAttribute.GroupName;
return modelMetadata;
}
}
And this extension method:
public static string GetGroupName(this ModelMetadata modelMetadata)
{
if (modelMetadata.AdditionalValues.ContainsKey(ExtendedDataAnnotationsModelMetadataProvider.Key_GroupName))
return (modelMetadata.AdditionalValues[ExtendedDataAnnotationsModelMetadataProvider.Key_GroupName] as string);
return null;
}
Source: http://bradwilson.typepad.com/blog/2010/01/why-you-dont-need-modelmetadataattributes.html
How About This !!! Must Work :
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Web;
using System.Web.Mvc;
namespace System.Web.Mvc
{
public static class DisplayGroup
{
public static MvcHtmlString DisplayGroupName(this HtmlHelper helper, string groupName)
{
return MvcHtmlString.Create(groupName);
}
public static MvcHtmlString DisplayGroupNameFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression)
{
var type = typeof(TModel);
PropertyInfo propertyInfo = null;
var member = (MemberExpression)expression.Body;
var property = (PropertyInfo)member.Member;
var name = property.Name;
var metadataTypeInfo = type.GetCustomAttribute<MetadataTypeAttribute>();
if (metadataTypeInfo != null)
{
var metadataType = metadataTypeInfo.MetadataClassType;
propertyInfo = metadataType.GetProperties().Where(x => x.Name == name).FirstOrDefault();
if (propertyInfo == null)
{
propertyInfo = type.GetProperties().Where(x => x.Name == name).FirstOrDefault();
}
}
else
{
propertyInfo = type.GetProperties().Where(x => x.Name == name).FirstOrDefault();
}
string output = "";
var dattr = propertyInfo.GetCustomAttribute<DisplayAttribute>();
if (dattr != null)
{
if (dattr.GroupName == null)
{
output = propertyInfo.Name;
}
else
{
output = dattr.GroupName;
}
}
else
{
output = propertyInfo.Name;
}
return MvcHtmlString.Create(output);
}
}
}
public class MyModel
{
[Display(Name = "Number",GroupName="Invoice")]
string InvNo { get; set; }
}
and then simply write :
#Html.DisplayGroupNameFor(x => x.InvNo)
Note :
NameSpace should be : System.Web.Mvc
Update :
The cool thing is that , if you have a MetaDataType class defined for your dataAnnotation , then also this will work as expected.
I've implemented localized validation, client-side, using the DataAnnotations attributes successfully. Now, I want to implement custom validation running server-side using the CustomValidationAttribute but my problem is that I can't find a way to get the client-side culture while executing the validation.
Here's the setup for the custom validation method:
public static ValidationResult ValidateField( string fieldValue, ValidationContext validationContext )
{
#if !SILVERLIGHT
// Get the message from the ValidationResources resx.
return new ValidationResult( ValidationResources.Message, new string[]{ "Field" } );
#else
return ValidationResult.Success;
#endif
}
This code returns the message but from the culture that the server is currently set.
I also tried to set the attribute on the property this way with same result:
[CustomValidation( typeof( CustomValidation ), "ValidateField", ErrorMessageResourceName = "Message", ErrorMessageResourceType = typeof( ValidationResources ) )]
I also tried to expose a method on my DomainService to change the Culture on the ValidationResources resx but this seems to be changing the culture not only or the current connection but for all the connections.
Since the validation is ran by Ria Services and not something I am calling directly, how can I tell the validation method to use a specific culture?
I came across this thread and I was able to fix my issue and have the culture name pass to every request made by the DomainContext (client) to the server.
First, we need to create a custom IClientMessageInspector that will be responsible to set a parameter for the CurrentUICulture for every requests.
public class AppendLanguageMessageInspector : IClientMessageInspector
{
#region IClientMessageInspector Members
public void AfterReceiveReply( ref Message reply, object correlationState )
{
// Nothing to do
}
public object BeforeSendRequest( ref Message request, IClientChannel channel )
{
var property = request.Properties[ HttpRequestMessageProperty.Name ] as HttpRequestMessageProperty;
if( property != null )
{
property.Headers[ "CultureName" ] = Thread.CurrentThread.CurrentUICulture.Name;
}
return null;
}
#endregion // IClientMessageInspector Members
}
Next, we need to create a custom WebHttpBehavior that will inject our custom IClientMessageInspector.
public class AppendLanguageHttpBehavior : WebHttpBehavior
{
public override void ApplyClientBehavior( ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.ClientRuntime clientRuntime )
{
clientRuntime.MessageInspectors.Add( _inspector );
}
private readonly AppendLanguageMessageInspector _inspector = new AppendLanguageMessageInspector();
}
Finally, we extend the client DomainContext.OnCreate method to add our custom WebHttpBehavior. NOTE: The namespace of the extended DomainContext class must be the same as the generated one...
public partial class DomainService1
{
partial void OnCreated()
{
var domainClient = this.DomainClient as WebDomainClient<IDomainService1Contract>;
if( domainClient != null )
{
domainClient.ChannelFactory.Endpoint.Behaviors.Add( DomainService1.AppendLanguageHttpBehavior );
}
}
private static readonly AppendLanguageHttpBehavior AppendLanguageHttpBehavior = new AppendLanguageHttpBehavior();
}
Now, on the server-side, when we want to get the language code we can simply access it like this:
var cultureName = System.Web.HttpContext.Current.Request.Headers[ "CultureName" ];
To enjoy even more of the DataAnnotation magic, we can even change the CurrentUICulture in the Initialize of the DomainService like this:
public override void Initialize( DomainServiceContext context )
{
var cultureName = System.Web.HttpContext.Current.Request.Headers[ "UICultureName" ];
Thread.CurrentThread.CurrentUICulture = new CultureInfo( cultureName );
base.Initialize( context );
}