c# - MVVM Validation in UWP -


in last week i've been trying apply mvvm pattern universal windows plataform, in elegant possible way, means apply solid principles , popular design patterns.

i've been trying reproduce exercise link: http://www.sullinger.us/blog/2014/7/4/custom-object-validation-in-winrt

also link windows 8 apps applies windows 10 apps according msdn answer @ forum: https://social.msdn.microsoft.com/forums/windowsapps/en-us/05690519-1937-4e3b-aa12-c6ca89e57266/uwp-what-is-the-recommended-approach-for-data-validation-in-uwp-windows-10?forum=wpdevelop

let me show classes, view final view:

<page x:class="validationtestuwp.mainpage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:validationtestuwp" xmlns:conv="using:validationtestuwp.converters" xmlns:viewmodels="using:validationtestuwp.viewmodel" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:ignorable="d">  <page.datacontext>     <viewmodels:accountcreationviewmodel/> </page.datacontext>  <page.resources>     <conv:validationmessageconverter x:key="validationmessageconverter"/> </page.resources>      <stackpanel grid.row="1"             verticalalignment="center"             horizontalalignment="center">      <!-- e-mail address input -->     <textblock text="email"                style="{staticresource titletextblockstyle}" />     <textbox x:name="emailtextbox"              margin="0 5 0 0"              minwidth="200"              text="{binding path=appuser.email, mode=twoway, updatesourcetrigger=propertychanged}"/>      <!--we have 1 more thing do. need update our xaml.          error textblocks bind validationmessages property within model,         using index matching property bound to.-->     <textblock x:name="emailvalidationerrortextblock"                text="{binding appuser.validationmessages[email], converter={staticresource validationmessageconverter}}"                foreground="red" />          <!-- password input -->     <textblock text="password"                margin="0 30 0 0"                style="{staticresource titletextblockstyle}"/>     <textbox x:name="passwordtextbox"              margin="0 5 0 0"              minwidth="{binding elementname=emailtextbox, path=minwidth}"              text="{binding path=appuser.validationmessages[password], converter={staticresource validationmessageconverter}}"/>      <textblock x:name="passwordvalidationtoshorterrortextblock"                text="{binding passwordtoshorterror}"                foreground="red" />     <textblock x:name="passwordvalidationtolongerrortextblock"                text="{binding passwordtolongerror}"                foreground="red" />      <!-- login command button -->     <button content="create account"             margin="0,10, 0, 0"             command="{binding createaccount}"/> </stackpanel> </page> 

my model end looking this: (also i'm adding explanation of classes within comments in classes.)

public class user : validatablebase {     private string email = string.empty;      public string email     {         { return email; }         set         {             email = value;             onpropertychanged("email");         }     }      private string password = string.empty;      public string password     {         { return password; }         set         {             password = value;             onpropertychanged("password");         }     }      /*now inheriting our base class, need implement required validate() method.      * in order keep single-responsibility-principle, invoke other methods within      * validate() method.       * since have validate multiple properties, should have each property validation contained       * within it's own method. makes easier test.*/     public override void validate()     {         validatepassword("password");         //base.onpropertychanged("password");         validateemail("email");         //base.onpropertychanged("email");          // passing in empty string cause validatablebase indexer hit.         // let ui refresh it's error bindings.         base.onpropertychanged(string.empty);     }      /*here invoke validatepassword , validateemail method.       * when done, notify observers entire object has changed not specifying property name      * in call onpropertychanged.       * lets observers (in case, view) know bindings need refreshed.*/     private ivalidationmessage validateemail(string property)     {         const string emailaddressemptyerror = "email address can not blank.";         if (string.isnullorempty(this.email))         {             var msg = new validationerrormessage(emailaddressemptyerror);             return msg;         }          return null;     }      private ivalidationmessage validatepassword(string property)     {         const string passwordtoshorterror = "password must minimum of 8 characters in length.";         const string passwordtolongerror = "password must not exceed 16 characters in length.";         if (this.password.length < 8)         {             var msg = new validationerrormessage(passwordtoshorterror);             return msg;         }         if (this.password.length > 16)         {             var msg = new validationerrormessage(passwordtolongerror);             return msg;         }          return null;     } 

this viewmodel:

/*view model   *   * next, need revise our view model.  * delete of error properties within it, along inotifypropertychanged implementation.  * need appuser property , icommand implementation.*/ public class accountcreationviewmodel {     public accountcreationviewmodel()     {         this.appuser = new user();         createaccount = new mycommand(createuseraccount);     }      private user appuser;      public event eventhandler canexecutechanged = delegate { };     public mycommand createaccount { get; set; }      public user appuser     {         { return appuser; }         set         {             appuser = value;         }     }      private void createuseraccount()     {         appuser.validate();          if (appuser.hasvalidationmessagetype<validationerrormessage>())         {             return;         }         // create user         // ......     }      /*now, when run app , enter invalid email or password,      * ui automatically inform of validation errors when press create account button.       * if ever need add more email validation (such proper email format)       * or more password validation (such not allowing specific characters) can without needing      * modify view model or view.      *       * if need add whole new property model, validation, can. don't need modify      * view model, need add textblock view display validation.*/ } 

also i've applied relaycommand pattern:

public class mycommand : icommand {     action _targetexecutemethod;     func<bool> _targetcanexecutemethod;      public mycommand(action executemethod)     {         _targetexecutemethod = executemethod;     }      public mycommand(action executemethod, func<bool> canexecutemethod)     {         _targetexecutemethod = executemethod;         _targetcanexecutemethod = canexecutemethod;     }      public void raisecanexecutechanged()     {         canexecutechanged?.invoke(this, eventargs.empty);     }      /*beware - should use weak references if command instance lifetime      longer lifetime of ui objects hooked command*/     // prism commands solve in implementation public event      public event eventhandler canexecutechanged = delegate { };      public bool canexecute(object parameter)     {         if (_targetcanexecutemethod != null)             return _targetcanexecutemethod();          if (_targetexecutemethod != null)             return true;          return false;     }      public void execute(object parameter)     {         /*this sintax use null*/         _targetexecutemethod?.invoke();     } } 

this fun begins, i'll introduce validatablebase created @ blog showed earlier:

public abstract class validatablebase : ivalidatable, inotifypropertychanged {     /*our initial class contains dictionary hold our validation messages.       * next, implement read-only property required our interface.*/     private dictionary<string, list<ivalidationmessage>> _validationmessages =          new dictionary<string, list<ivalidationmessage>>();      /*the call onpropertychanged let ui know collection has changed.       * in cases won't used since collection read-only, since going in base class,      * want provide support that.*/     public dictionary<string, list<ivalidationmessage>> validationmessages     {         { return _validationmessages; }         set         {             _validationmessages = value;             onpropertychanged("validationmessages");         }     }      public event propertychangedeventhandler propertychanged = delegate { };      /*our base class implements inotifypropertychanged method,       * remove our model , put implementation in our base class.*/     public void onpropertychanged(string propertyname)     {         propertychanged?.invoke(this, new propertychangedeventargs(propertyname));     }      /*in method, check if collection contains key matching property supplied.      * if does, check it's values see if of them match type specified in < t>.      * lets      *       * hasvalidationmessagetype< validationerrormessage>("email");      *       * check if model has validation error on email property.*/     public bool hasvalidationmessagetype<t>(string property = "")     {         if (string.isnullorempty(property))         {             bool result = _validationmessages.values.any(collection =>                 collection.any(msg => msg t));             return result;         }          return _validationmessages.containskey(property);     }      /*in method create new collection if key doesn't exist yet,      * double check ensure validation message not exist in collection.      * if not, add it.*/     public void addvalidationmessage(ivalidationmessage message, string property = "")     {         if (string.isnullorempty(property))         {             return;         }          // if key not exist, create one.         if (!_validationmessages.containskey(property))         {             _validationmessages[property] = new list<ivalidationmessage>();         }          if (_validationmessages[property].any(msg => msg.message.equals(message.message) || msg == message))         {             return;         }          _validationmessages[property].add(message);     }      /*here check if there message supplied key , remove it.      * @ moment, not type checking see if there more       * 1 type of object (warning , error) in collection same message.      * method removes first thing finds , calls good.*/     public void removevalidationmessage(string message, string property = "")     {         if (string.isnullorempty(property))         {             return;         }          if (!_validationmessages.containskey(property))         {             return;         }          if (_validationmessages[property].any(msg => msg.message.equals(message)))         {             // remove error key's collection.             _validationmessages[property].remove(                 _validationmessages[property].firstordefault(msg => msg.message.equals(message)));         }     }      /*we check if key exists matches property name , clear out messages contents      * , remove key dictionary.*/     public void removevalidationmessages(string property = "")     {         if (string.isnullorempty(property))         {             return;         }          if (!_validationmessages.containskey(property))         {             return;         }          _validationmessages[property].clear();         _validationmessages.remove(property);     }      /*finally, finish implementing interface building validateproperty method.      * in method, invoke delegate provided, , accept ivalidationmessage object in return.      * if return value not null, add validationmessages collection.      * if null, can assume validation passed , there no issues.      * since case, remove validation collection.*/     public ivalidationmessage validateproperty(func<string, ivalidationmessage> validationdelegate,         string failuremessage, string propertyname = "")     {         ivalidationmessage result = validationdelegate(failuremessage);         if (result != null)         {             this.addvalidationmessage(result, propertyname);         }         else         {             this.removevalidationmessage(failuremessage, propertyname);         }          return result;     }      /*we have satisfied requirements of ivalidatable interface, there 1 more method      * need add base class. let group of our property validations in single call.      *       * mark abstract, since base class has nothing validate,       * , want force object inherits base class implement method.       * if don't want this, can opt out of in code. not needs have feature,      * reason why left out of interface.*/     public abstract void validate(); } 

lastly interfaces:

//the first thing did created interface models needing validation required implement.  public interface ivalidatable {     /*this read-only property, contain of our validation messages.       * property has key typed string, models property name.       * value collection of ivalidationmessage objects (we discuss ivalidationmessage later).      * idea being each property in model, can store more 1 error.*/     dictionary<string, list<ivalidationmessage>> validationmessages { get; }      /*this method used add validation message validationmessages collection.      * property assigned key, message being added value.*/     void addvalidationmessage(ivalidationmessage message, string property = "");      /*just can add validation message, provide ourselves ability remove it.*/     void removevalidationmessage(string message, string property = "");      /*we can use method clear out validation messages in 1 shot single property.*/     void removevalidationmessages(string property = "");      /*this method return true if object has validation messages matching <t> , false if not.*/     bool hasvalidationmessagetype<t>(string property = "");      /*this method can called perform validation on property within object ,       * build collection of errors. arguments require method delegate returns ivalidationmessage object.      * how validation becomes reusable. each individual object can pass in method delegate performs      * actual validation. ivalidatable implementation take results , determine if must go in      * validationmessages collection or not.*/     ivalidationmessage validateproperty(func<string, ivalidationmessage> validationdelegate,          string failuremessage,         string propertyname = ""); }  /*the idea this, can create objects implement interface,  * containing different types of messages. instance, in post, create validationerrormessage  * , validationwarningmessage. go on , create kind of messaging want , use  * binding view.*/ public interface ivalidationmessage {     string message { get; } } 

this converter:

/*the idea this, can create objects implement interface,  * containing different types of messages. instance, in post, create validationerrormessage  * , validationwarningmessage. go on , create kind of messaging want , use  * binding view.*/ public interface ivalidationmessage {     string message { get; } } 

and validationerrormessages:

 /*before end post, show 2 implementations of ivalidationmessage.  * both same thing, typed differently can segregate messages type.  * gives more flexibility using enum.  *   * first error validation message.*/ public class validationerrormessage : ivalidationmessage {     public validationerrormessage() : this(string.empty)     { }      public validationerrormessage(string message)     {         this.message = message;     }      public string message { get; private set; } } 

now every time run code code in example shown @ sullinger blog exception:

system.reflection.targetinvocationexception: 'the text associated error code not found.

i'm using vs2017 i'm trying apply mvvm pattern validation in uwp, of course validation on viewmodel every field implies i'll have write validation every view created , far see in example, save me tons of code.

does understand wrong code?

i don't want use tools mvvm light or mvvm cross or prism purely custom mvvm on uwp.

ok able make code work, has fixes able understand , solve on own, before publish answer because have 2 answers problem, i'll apologize community, didnt want ask me, wasnt intention, if looked that, i'm sorry, i'll try sound less needy.

well down solution:

the main issue of code i've published in abstract method validate, since gotta write own validation each field , control adding , removal of error messages, wrote validate method one:

public override void validate()     {         removevalidationmessages("password");         removevalidationmessages("email");         addvalidationmessage(validatepassword("password"), "password");         addvalidationmessage(validateemail("email"), "email");          // passing in empty string cause validatablebase indexer hit.         // let ui refresh it's error bindings.         base.onpropertychanged(string.empty);     } 

as can see i've begin method removing message not add more once message, since addvalidation message doesnt allow repeat same error message. use addvalidationmessagemethod within method use our custom validate methods password or email return message add here in our custom methods issue, return null message everytime converter trigger throwed exception showed @ question. in order solve instead of returning null when textboxes had text returned empty constructor of validationerrormessages class methods:

private ivalidationmessage validateemail(string property)     {         const string emailaddressemptyerror = "email address can not blank.";         if (string.isnullorempty(this.email))         {             var msg = new validationerrormessage(emailaddressemptyerror);             return msg;         }          return new validationerrormessage();     }      private ivalidationmessage validatepassword(string property)     {         const string passwordtoshorterror = "password must minimum of 8 characters in length.";         const string passwordtolongerror = "password must not exceed 16 characters in length.";         if (this.password.length < 8)         {             var msg = new validationerrormessage(passwordtoshorterror);             return msg;         }         if (this.password.length > 16)         {             var msg = new validationerrormessage(passwordtolongerror);             return msg;         }          return new validationerrormessage();     } 

this solve problem of triggering exception. leave methods returning null have modify converter checks ivalidationmessages collection null values.

it this:

 public object convert(object value, type targettype, object parameter, string language)     {         if (!(value ienumerable<ivalidationmessage>))         {             return string.empty;         }          var collection = value ienumerable<ivalidationmessage>;         if (!collection.any())         {             return string.empty;         }          if (collection.firstordefault() == null)         {            return string.empty;         }                     return collection.firstordefault().message;     } 

this way can update our error messages fields , guy can have working mvvm validation working pattern uwp. highly testable, maintainable , extensible app.

hope solution digging @ uwp. there great articles many different sources try sullinger blog approach i'm adopting wintellect has article i'll share link here: http://www.wintellect.com/devcenter/jlikness/simple-validation-with-mvvm-for-windows-store-apps

this work uwp i've tested gotta work bit more. , jerry nixon has great article uwp validation too, model way more elegant sullinger link nixon validation: http://blog.jerrynixon.com/2014/07/lets-code-handling-validation-in-your.html

and has source code here: http://xaml.codeplex.com/sourcecontrol/latest#blog/201406-validation/app10/common/modelbase.cs

well hope else. questions i'll glad too.


Comments

Popular posts from this blog

Command prompt result in label. Python 2.7 -

javascript - How do I use URL parameters to change link href on page? -

amazon web services - AWS Route53 Trying To Get Site To Resolve To www -