What is the best way to do this for a WP7 7.0 app (SL3). Should I use visual states? IS there a way to use attributes from the model sort of like the Silverlight examples would do ? Any good examples with MVVM+WP7?
So far all I can find is these two examples.
http://babaandthepigman.wordpress.com/2010/10/21/simple-textbox-validation-wp7/
http://www.windowsphonegeek.com/articles/Building-WP7-Custom-Validation-Control---Architecture-amp-Basic-Prototype
For every view you should to make files named Validated(ViewName).cs. Everyone should inherit from ValidatedModelBase implemented interfaces INotifyPropertyChanged, INotifiDataError.
Properties of ValidationModelBase:
IsValid
ShowErrors
Methods:
T ToModel - convert all ValidatedModel fields to ViewModel
void Validate:
public void Validate()
{
var fields = new List<string>();
var type = this.GetType();
var baseType = typeof(ValidatedModelBase<T>);
while (type != baseType)
{
fields.AddRange(type.GetFields()
.Where(field => field.FieldType == typeof(String) && field.Name.EndsWith("PropertyName"))
.Select(field => field.GetValue(this) as String));
type = type.BaseType;
}
foreach (var field in fields)
{
this.SetErrors(field, this.Validate(field), false);
}
this.RefreshIsValid();
}
virtual IList Validate(string propertyName) - it will be implemented in ValidatedModels, where You give property name to validate as parameters and handle it in simple switch/case instruction.
I can write more about my method, if You want.
Related
ASP.NET Core MVC has a great model binding & model validation subsystem which supports almost any scenario. But when developing APIs things can go a little more complicated.
Suppose we have a model class with a property XYZ which is annotated with [MinLength(5)].
public class ViewModel
{
[MinLength(5)]
public string XYZ { get; set; }
}
If anything goes wrong with this property what MVC will give you is something like this:
{ "XYZ": [ "The field XYZ must be a string or array type with minimum length of '5'" ] }
But this is not what the client needs! The client needs an object with specific details. She will create her own message however she wants to:
{ "error": "minLength", "property": "XYZ", "minimum": 5 }
Possible Solutions:
You can use InvalidModelStateResponseFactory to generate customized responses. It gives you the ActionContext which contains the ModelState property. But all you can do is to process error messages which are pure strings! That could lead to some problems.
Another option is to completely disable the MVC Validation and implement one for yourself.
I appreciate any other solutions.
For general validation message, it is pure string. And for minLength and minimum are different for different validation attribute. I am wondering how the client will check the different node.
For server side, InvalidModelStateResponseFactory would be better to return json object. and you need to check the ValidationAttribute for return different object like
services.Configure<ApiBehaviorOptions>(o =>
{
o.InvalidModelStateResponseFactory = actionContext =>
{
var error = new Dictionary<string, string>();
foreach (var key in actionContext.ModelState.Keys)
{
foreach (var parameter in actionContext.ActionDescriptor.Parameters)
{
var prop = parameter.ParameterType.GetProperty(key);
if (prop != null)
{
var attr = prop.GetCustomAttributes(typeof(ValidationAttribute), false).FirstOrDefault() as ValidationAttribute;
if (attr is MinLengthAttribute minLengthAttribute)
{
error.Add("Error", "minLength");
error.Add("Property", key);
error.Add("minimum", minLengthAttribute.Length.ToString());
}
}
}
}
return new BadRequestObjectResult(error);
};
});
I have a CalendarView that looks like this:
<CalendarView
android:layout_width="match_parent"
android:layout_height="300dp"
android:id="#+id/createReservationCalendarView" />
Here is how I handle the DateChange event without MvvmCross:
protected override void OnCreate(Bundle savedInstanceState)
{
... Code ...
calendar.DateChange += (s, args) =>
{
var year = args.Year;
var month = args.Month + 1;
var dayOfMont = args.DayOfMonth;
var date = new DateTime(year, month, dayOfMont);
var myReservations = new Intent(this, typeof(CreateReservationTimeslotScreen));
myReservations.PutExtra("selectedDate", date.Ticks);
StartActivity(myReservations);
};
}
Now that I have switched to MvvmCross, I would like to have my ViewModel start the new activity instead.
Im not sure how to do this, since the ViewModel should be OS and UI agnostic.
The "args" argument is of type CalendarView.DateChangeEventArgs, which is Android specific, so I cant use that in the ViewModel. It derives from System.EventArgs, so maybe I could use that instead. I am thinking that there must be a simpler way.
A thought that I had was if it is possible to update a property on the ViewModel from the activity, and then execute the switch to the new Activity from there? I'm not sure how this could be accomplished since activites dont have references to their ViewModels.
Any suggestions?
Thanks.
MvvmCross does give you access to your ViewModel from your View. The relationship between your View (e.g. Activity/fragment in Android) and your ViewModel, and their ability to share data (models) in both directions is a core characteristic a Mvvm framework.
In order to setup an Activity to be used with MvvmCross you need to make sure to inherit from MvxActivity or MvxAppCompatActivity (If using Android Support Library). Following which you need to link your Activity to its corresponding ViewModel using one of the possible conventions (See link, for basic sample of each registration offered by the MvxViewModelViewTypeFinder). A simple example would be to use the concrete type based registration using the type parameter overload.
public class FirstActivity : MvxAppCompatActivity<FirstViewModel>
Now that you have access to your ViewModel from your View you can create a command that can be used to execute the navigation:
CalendarViewModel (ViewModel linked to the current Activity in question)
Create a command that requires a DateTime parameter, which in turn will pass the value when navigation (see MvvmCross Navigation docs for alternative navigation and parameter passing conventions).
public class CalendarViewModel : MvxViewModel
{
IMvxCommand _goToMyReservationCommand;
public IMvxCommand GoToMyReservationCommand =>
_goToMyReservationCommand ??
(_goToMyReservationCommand = new MvxCommand<DateTime>(NavigateToMyReservation));
void NavigateToMyReservation(DateTime reservationDate)
{
ShowViewModel<MyReservationViewModel>(
new GoToMyReservationParameter
{
ReservationTicks = reservationDate.Ticks
});
}
}
Navigation Parameter Class
Holds the values and type information used for navigation.
public class GoToMyReservationParameter
{
public long ReservationTicks { get; set; }
}
MyReservationViewModel
The ViewModel that will receive the value passed.
public class MyReservationViewModel : MvxViewModel
{
public void Init(GoToMyReservationParameter parameters)
{
var reservationTicks = parameters.ReservationTicks;
// Do what you need with the parameters
}
}
View
Execute the command on the ViewModel and pass through the DateTime object.
public class CalendarActivity : MvxAppCompatActivity<CalendarViewModel>
{
protected override void OnCreate(Bundle bundle)
{
... Code...
calendar.DateChange += (s, args) =>
{
var year = args.Year;
var month = args.Month + 1;
var dayOfMont = args.DayOfMonth;
var date = new DateTime(year, month, dayOfMont);
ViewModel.GoToMyReservationCommand.Execute(date);
};
}
}
Can't find the proper solution to this problem.
I am using [Serializable] (MVC3 Futures) in order to have a "wizard" with separate views. Here is the code in my controller to serialize:
private MyViewModel myData;
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
var serialized = Request.Form["myData"];
if (serialized != null) //Form was posted containing serialized data
{
myData = (MyViewModel)new MvcSerializer().Deserialize(serialized, SerializationMode.Signed);
TryUpdateModel(myData);
}
else
myData = (MyViewModel)TempData["myData"] ?? new MyViewModel();
TempData.Keep();
}
protected override void OnResultExecuted(ResultExecutedContext filterContext)
{
if (filterContext.Result is RedirectToRouteResult)
TempData["myData"] = myData;
}
Further along in my controller I do something like this (just a snippet - code goes through wizard with next and back button strings):
public ActionResult Confirm(string backButton, string nextButton)
{
if (backButton != null)
return RedirectToAction("Details");
else if ((nextButton != null) && ModelState.IsValid)
return RedirectToAction("Submitted");
else
return View(myData);
}
In my .cshtml view, I have this:
#using (Html.BeginFormAntiForgeryPost())
{
#Html.Hidden("myData", new MvcSerializer().Serialize(Model, SerializationMode.Signed))
...
#Html.TextBoxFor(m => model.Step.EMail)
...
}
Because I am using dynamics, I have to use a variable instead in the view:
var model = (MyViewModel) Model.myData;
in order to do the #Html.TextBoxFor above. And herein lies my probelm, because if I do #model MyViewModel instead, then I can't do model.Step.EMail. But because of dynamics, the #Html.Hidden won't work and I get the following error:
Compiler Error Message: CS1973: 'System.Web.Mvc.HtmlHelper'
has no applicable method named 'Hidden' but appears to have an
extension method by that name. Extension methods cannot be dynamically
dispatched. Consider casting the dynamic arguments or calling the
extension method without the extension method syntax.
I can switch to some other way of doing this without [Serializable], but then I have to convert a LOT of code. Is there any way to make this work?
The extension method is not identifying the method because, the data type does not match.
Try cast as object.
#Html.Hidden("myData", new MvcSerializer().Serialize(Model, SerializationMode.Signed) as Object)
or
#Html.Hidden("myData", (Object)new MvcSerializer().Serialize(Model, SerializationMode.Signed))
It will works.
You can call
#(InputExtensions.Hidden(Html, "myData", new MvcSerializer().Serialize(Model, SerializationMode.Signed)))
instead of #Html.Hidden(...)
It is calling the extension method without the extension method syntax.
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.
Here's a scenario:
I have an autocomplete plugin (custom) that keeps a hidden field of JSON objects (using a specific struct).
I've created an Html helper that helps me easily bind to a specific custom model (basically, it has a JSON property that is for two-way binding and a property that lets me deserialize the JSON into the appropriate struct):
public class AutoCompleteModel {
public string JSON { get; set; }
public IEnumerable<Person> People {
get {
return new JavaScriptSerializer().Deserialize<Person>(this.JSON);
}
set {
this.JSON = new JavaScriptSerializer().Serialize(value);
}
}
}
This works great and I can model bind using the default binder #Html.Autocomplete(viewModel => viewModel.AutoCompleteModelTest). The HTML helper generates HTML like:
<input type="text" id="AutoCompleteModelTest_ac" name="AutoCompleteModelTest_ac" value="" />
<input type="hidden" id="AutoCompleteModelTest_JSON" name="AutoCompleteModelTest.JSON" value="{JSON}" />
The problem is this is not the best way for consumers. They have to manually set the People property to an array of Person structs. In my data layer, my domain objects probably will not be storing the full struct, only the person's ID (a corporate ID). The autocomplete will take care of looking up the person itself if only given an ID.
The best scenario will be to call it like this:
#Html.Autocomplete(domainObject => domainObject.PersonID) or
#Html.Autocomplete(domainObject => domainObject.ListOfPersonIDs
I would like it to work against the string property AND against the custom AutoCompleteModel. The autocompleter only updates a single hidden field, and that field name is passed back on postback (the value looks like: [{ "Id":"12345", "FullName":"A Name"},{ "Id":"12347", "FullName":"Another Name" }]).
The problem is, of course, that those domain object properties only have an ID or array of IDs, not a full Person struct (so cannot be directly serialized into JSON). In the HTML helper, I can transform those property values into a struct, but I don't know how to transform it back into a simple type on POST. The solution I need would transform an ID into a new Person struct on page load, serializing it into the hidden field. On POST, it would deserialize the generated JSON back into a simple array of IDs.
Is a custom model binder the solution I need? How can I tell it to work both with a custom model AND simple types (because I don't want it applied to EVERY string property, just need it to deal with the values given by the HTML helper).
I figured it out, it's possible!
To clarify, I needed to: transform a string or string array (of IDs) into a JSON structure for my hidden field value, then on post back, deserialize the JSON in the hidden field and transform the struct back into a simple string or string array (of IDs) for my domain object's property.
Step 1: Create a HTML helper
I had done this already, but only for accepting my custom AutoCompleteModel type. I needed one for a string and an Enumerable of string type.
All I did was generate my Person struct(s) from the value of the property and serialize them into JSON for the hidden field the Autocompleter uses (this is an example of the string helper, I also have a nearly identical one for IEnumerable<string>):
public static MvcHtmlString AutoComplete<TModel>(
this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, string>> idProp)
where TModel : class
{
TModel model = htmlHelper.ViewData.Model;
string id = idProp.Compile().Invoke(model);
string propertyName = idProp.GetPropertyName();
Person[] people = new Person[] {
new Person() { ID = id }
};
// Don't name the textbox the same name as the property,
// otherwise the value will be whatever the textbox is,
// if you care.
MvcHtmlString textBox = htmlHelper.TextBox(propertyName + "_ac", string.Empty);
// For me, the JSON is the value I want to postback
MvcHtmlString hidden = htmlHelper.Hidden(propertyName, new JavaScriptSerializer().Serialize(people));
return MvcHtmlString.Create(
"<span class=\"AutoComplete\">" +
textBox.ToHtmlString() +
hidden.ToHtmlString() +
"</span>");
}
Usage: #Html.AutoComplete(model => model.ID)
Step 2: Create a custom model binder
The crux of my issue was that I needed this binder to only apply to certain properties, and they were strings or string arrays.
I was inspired by this article because it used Generics. I decided, hey, we can just ask people what property they want to apply the binder for.
public class AutoCompleteBinder<T> : DefaultModelBinder
where T : class
{
private IEnumerable<string> PropertyNames { get; set; }
public AutoCompleteBinder(params Expression<Func<T, object>>[] idProperties)
{
this.PropertyNames = idProperties.Select(x => x.GetPropertyName());
}
protected override object GetPropertyValue(
ControllerContext controllerContext,
ModelBindingContext bindingContext,
PropertyDescriptor propertyDescriptor,
IModelBinder propertyBinder)
{
var submittedValue = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (submittedValue != null && this.PropertyNames.Contains(propertyDescriptor.Name))
{
string json = submittedValue.AttemptedValue;
Person[] people = new JavaScriptSerializer().Deserialize<Person[]>(json);
if (people != null && people.Any())
{
string[] IDs = people.Where(x => !string.IsNullOrEmpty(x.ID)).Select(x => x.ID).ToArray();
bool isArray = bindingContext.ModelType != typeof(string) &&
(bindingContext.ModelType == typeof(string[]) ||
bindingContext.ModelType.HasInterface<IEnumerable>());
if (IDs.Count() == 1 && !isArray)
return IDs.First(); // return string
else if (IDs.Count() > 0 && isArray)
return IDs.ToArray(); // return string[]
else
return null;
}
else
{
return null;
}
}
return base.GetPropertyValue(controllerContext, bindingContext, propertyDescriptor, propertyBinder);
}
}
GetPropertyName() (translate LINQ expression into a string, i.e. m => m.ID = ID) and HasInterface() are just two utility methods I have.
Step 3: Register
Register the binder on your domain objects and their properties in Application_Start:
ModelBinders.Binders.Add(typeof(Employee), new AutoCompleteBinder<Employee>(e => e.ID, e => e.TeamIDs));
It's only a little bit annoying to have to register the binder for specific properties, but it's not the end of the world and provides a nice, smooth experience working with my autocompleter.
Any comments are welcome.