In this post I will write about some problems I faced with INotifyPropertyChanged while using MVVM and some solutions available.
Let’s first examine the problem.
Is INotifyPropertyChanged an anti-pattern or not?
The short answer is yes? But it really depends on how big is the project. It is very common to use PropertyChanged events inside ViewModels of an MVVM application to propagate changes occurring in your classes back to presentation layer via WPF data binding mechanism.
So why the size of the project matters. Well, it all comes down to maintainability of the code base. PropertyChange events are usually raised by passing property names as string constants (that’s a bummer). If the number of view models and their properties is large, the project will have too many hard coded strings. Yes, it is possible to move these strings at the beginning of the class file, and there are very nice tools which can do that for you, such as Refactor! from DevExpress or even Visual Studio itself. But still if you are changing schema and your data model is regenerated then most likely corresponding view models will require changes as well and that is where maintenance becomes a nightmare on projects with more than 10 view models. Add to it all the business complexities and tracking control flow will be almost impossible. So the question basically is do you want to pollute your code with hardcoded string? If code is large – then probably NO, if code is small then I guess it’s OK.
Now let’s see what are the options (this list is not extensive).
1. Why not use reflection to get property names, when property names change we can simply recompile? So such solution was proposed by Karl Shifflett – a Program Manager on Patterns and Practices Prism Team, Microsoft Corporation. But later Sacha Barber found a flaw in such approach with Stack Frames. Karl since updated his post and commented on this problem. Basically if you compile your code for release and remove pdb (debug database with initial method and property names) files, due to compiler optimization the property names will not be properly resolved and some calls will end up in the wrong place inside the stack :(. Sacha since proposed his own framework for MVVM called Cinch. While it sounds like a solid piece of work it doesn’t really address PropertyChanged problem.
2. Josh Smith blogged about this problem as well and proposed using lambda expressions to validate property names at compile time, thus avoiding StackFrame problem. He used a property observer pattern and created a generic class (PropertyObserver<TPropertySource>) to handle it along with weak event listeners. Still this approach requires changes to be propagates when properties change. While again there are tools which can do this in semi-automated fashion it could be error prone when there are too many dependencies between properties within and outside of the class.
3. Another approach is to use weak event referencing for all independent properties. Such solution called UpdateControls was proposed by Michael Perry. He has a great set of videos on his web site explaining this approach. And this is wonderful, but it because an overkill when working with large collections. It takes up too much memory to register for every single independent property in let’s say a list of 100 000 records.
4. My approach. I decided to use a mix of things from the above.
- All my Data Model classes are generated and use Property Changed events. Since there is no business logic which resides in these classes there is no complexity associated in maintaining custom event handlers or raising of events. Everything is generated and regenerated automatically when database schema changes.
- For my view models I used a similar approach. But use UpdateControl for the properties. Partial view model classes are generated based off of a database schema (dbml XML model file) using slightly modified T4 template provided by Damien. Here is my template below.
<# // L2ST4 - LINQ to SQL templates for T4 v0.85 - http://www.codeplex.com/l2st4 // Copyright (c) Microsoft Corporation. All rights reserved. // This source code is made available under the terms of the Microsoft Public License (MS-PL) #><#@ template language="C#v3.5" hostspecific="True" #><#@ include file="..\\DataModels\\L2ST4.ttinclude" #><#@ assembly name="..\My Documents\Visual Studio 2008\Projects\SHMMP\ShmmpManager\ShmmpManager\bin\Debug\ShmmpManager.exe" #><#@ import namespace="ShmmpManager.ViewModels" #><#@ output extension=".generated.cs" #><# // Set options here var options = new { DbmlFileName = Host.TemplateFile.Replace("ViewModel.tt","Data.dbml").Replace("ViewModels","DataModels"), // Which DBML file to operate on (same filename as template) SerializeDataContractSP1 = false, // Emit SP1 DataContract serializer attributes FilePerEntity = true, // Put each class into a separate file StoredProcedureConcurrency = false, // Table updates via an SP require @@rowcount to be returned to enable concurrency }; var code = new CSharpCodeLanguage(); var data = new Data(options.DbmlFileName); var manager = Manager.Create(Host, GenerationEnvironment); data.ContextNamespace = (new string[] { manager.DefaultProjectNamespace }).FirstOrDefault(s => !String.IsNullOrEmpty(s)); data.EntityNamespace = (new string[] { manager.DefaultProjectNamespace }).FirstOrDefault(s => !String.IsNullOrEmpty(s)); string baseClassName = "EntityViewModelBase"; string entityBase = "ViewModelBase"; manager.StartHeader(); #>#pragma warning disable 1591 //------------------------------------------------------------------------------ // <auto-generated> // This code was generated by LINQ to SQL template for T4 C# // Generated at <#=DateTime.Now#> // The original template was modified by Ivan to meet certain // project related requirements. // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // </auto-generated> //------------------------------------------------------------------------------ using System; using System.Text; using System.Linq; using System.Data.Linq; using System.Data.Linq.Mapping; using System.Collections.Generic; using ShmmpManager.DataModels; <#if (data.Functions.Count > 0) {#> using System.Reflection; <#} string dataContractAttributes = (options.SerializeDataContractSP1) ? "IsReference=true" : ""; if (data.Serialization) {#> using System.Runtime.Serialization; <#}#> using UpdateControls; <# manager.EndBlock(); foreach(Table table in data.Tables) { foreach(TableClass class1 in table.Classes) { manager.StartNewFile(Path.ChangeExtension(class1.Name + "ViewModel",".generated.cs")); if (!String.IsNullOrEmpty(data.EntityNamespace)) {#> namespace <#=data.EntityNamespace#>.ViewModels { <# } if (data.Serialization && class1.IsSerializable) { #> [DataContract(<#=dataContractAttributes#>)] <# } if (class1 == table.BaseClass) {#> <# foreach(TableClass subclass in data.TableClasses.Where(c => c.Table == table)) { if (!String.IsNullOrEmpty(subclass.InheritanceCode)) {#> [InheritanceMapping(Code=@"<#=subclass.InheritanceCode#>", Type=typeof(<#=subclass.Name#>)<# if (subclass.IsInheritanceDefault) {#>, IsDefault=true<#}#>)] <# } if (data.Serialization && subclass.IsSerializable) {#>[KnownType(typeof(<#=subclass.Name#>))]<#} } #> <#=code.Format(class1.TypeAttributes)#>partial class <#=class1.Name#>ViewModel : <# if (class1.Name == "EquipmentPremiumRisk" || class1.Name == "EquipmentPremiumManaged" || class1.Name == "EquipmentPremiumShared" || class1.Name == "EquipmentPremiumCapped" || class1.Name == "EquipmentPremiumDirectedService"){ #> EquipmentPremiumViewModelBase <#} else if (class1.Name == "EquipmentPremiumAdjustmentRisk" || class1.Name == "EquipmentPremiumAdjustmentManaged" || class1.Name == "EquipmentPremiumAdjustmentShared" || class1.Name == "EquipmentPremiumAdjustmentCapped" || class1.Name == "EquipmentPremiumAdjustmentDirectedService"){ #> EquipmentPremiumAdjustmentViewModelBase <#} else if (!String.IsNullOrEmpty(entityBase)) {#> <#=baseClassName#> <# }#> <# else { #><#=class1.SuperClass.Name#> <#}}#> { #region Fields // list to support error validations protected new static List<PropertyRuleBase<ViewModelBase>> _propertiesRules = new List<PropertyRuleBase<ViewModelBase>>(); // list to support warnings validations. protected new static List<PropertyRuleBase<ViewModelBase>> _propertiesWarnings = new List<PropertyRuleBase<ViewModelBase>>(); // static list to support singleton pattern. private static List<<#=class1.Name#>ViewModel> _instances = new List<<#=class1.Name#>ViewModel>(); #endregion #region Data Members private <#=class1.Name#> _<#= class1.Name #>; #endregion #region Properties public override bool IsNew { get { <# if (class1.Name == "Agent" || class1.Name == "Message" || class1.Name == "TypeCoverageDetail" || class1.Name == "ErrorLog" || class1.Name.StartsWith("vw") ){#> return _<#= class1.Name #>.<#= class1.PrimaryKey[0].Name #> == default(<#= class1.PrimaryKey[0].Type #>); <# }else{#> return String.IsNullOrEmpty(_<#= class1.Name #>.AddedByStaffCode); <# }#> } } <# if (class1.Name != "ErrorLog" && !class1.Name.StartsWith("vw") ) { #> private Independent _indAudit = new Independent(); public IQueryable<<#= class1.Name #>Audit> Audit { get { _indAudit.OnGet(); return from <#=class1.Name#>Audit c in ShmmpAudit.<#=class1.Name#>Audits where c.<#= class1.PrimaryKey[0].Name #> == _<#= class1.Name #>.<#= class1.PrimaryKey[0].Name #> orderby c.DateStamp descending select c; } } private Independent _indHistory = new Independent(); public IQueryable<<#= class1.Name #>Audit> History { get { _indHistory.OnGet(); return (new List<<#= class1.Name #>Audit>()).AsQueryable(); // TODO: write history query, needs to return IEnumerable<ENTITYHistory> } } public override string HistoryMessage { get { string historyMessage = String.Empty; // getting <#=class1.Name#> Audit <#=class1.QualifiedName#>Audit auditRecord = (AuditRecord as <#=class1.QualifiedName#>Audit); if (auditRecord != null) { historyMessage = String.Format("Historical reference for: \nDate - ({0:d}), Time - ({1:T}), Operation - ({2}), Staff - ({3}).", auditRecord.DateStamp.Date, auditRecord.DateStamp, auditRecord.TableOperation.Description, auditRecord.StaffCode); } //<#=class1.Name#>History historyRecord = (HistoryRecord as <#=class1.Name#>History); //if (historyRecord != null) //{ // historyMessage = String.Format("Historical reference for: \nBill Period - ({0}).", // historyRecord.BillPeriod.Date); //} return historyMessage; } } <# } #> #endregion //Properties #region Construction /// <summary> /// This is static constructor to emulate singleton pattern. /// </summary> /// <param name="<#=class1.Name.ToLower()#>">Entity record.</param> /// <param name="baseTable">Base Table which stores current entity.</param> /// <returns></returns> public static <#=class1.Name#>ViewModel Create(<#=class1.Name#> <#=class1.Name.ToLower()#>,ITable baseTable) { <#=class1.Name#>ViewModel instance =null; //= _instances.FirstOrDefault(i=> i.DataEntity == <#=class1.Name.ToLower()#>); //if (instance == null) //{ if (<#=class1.Name.ToLower()#> != null) { if (_propertiesRules == null) { _propertiesRules = new List<PropertyRuleBase<ViewModelBase>>(); } if (_propertiesWarnings == null) { _propertiesWarnings = new List<PropertyRuleBase<ViewModelBase>>(); } instance = new <#=class1.Name#>ViewModel(<#=class1.Name.ToLower()#>, baseTable); //_instances.Add(instance); } //} return instance; } private <#=class1.Name#>ViewModel(<#=class1.Name#> <#=class1.Name.ToLower()#>,ITable baseTable) { BaseTable = baseTable; DataEntity = <#=class1.Name.ToLower()#>; _<#= class1.Name#> = <#=class1.Name.ToLower()#>; <# if (class1.HasPrimaryKey){#> // initializing primary and foreign keys <# foreach(Column column in class1.Columns) { #> <#=column.Storage #> = _<#= class1.Name#>.<#= column.Member #>; <# } }#> // initializing data context DataContext = baseTable.Context as <#=data.ContextName#>; _propertyChangedHandler = new System.ComponentModel.PropertyChangedEventHandler(DataEntity_PropertyChanged); _<#= class1.Name#>.PropertyChanged += _propertyChangedHandler; Initialize(); <# if(class1.Name != "ErrorLog" && !class1.Name.StartsWith("vw") ) {#> _saveHandler = OnSaveEvent; this.SavedEvent += _saveHandler; <# }#> } partial void Initialize(); #endregion <# int dataMemberIndex = 1; if (class1.Columns.Count > 0) { #> #region Column Mappings <# foreach(Column column in class1.Columns) { #> //data member private <#=code.Format(column.StorageType)#> <#=column.Storage#><# if (column.IsReadOnly ) {#> = default(<#=code.Format(column.StorageType)#>)<#}#>; private static PropertyRuleBase<ViewModelBase> <#=column.Member#>PropertyRules; private static PropertyRuleBase<ViewModelBase> <#=column.Member#>PropertyWarnings; <# if (!column.IsPrimaryKey && !column.IsReadOnly){#> private Independent _ind<#=column.Member#> = new Independent(); <# }#> <# #> <#=code.Format(column.MemberAttributes)#><# if (((class1.Name == "EquipmentPremiumRisk" || class1.Name == "EquipmentPremiumManaged" || class1.Name == "EquipmentPremiumShared" || class1.Name == "EquipmentPremiumCapped" || class1.Name == "EquipmentPremiumDirectedService") &&(typeof(EquipmentPremiumViewModelBase) .GetMember(column.Member).FirstOrDefault() != null)) || ((class1.Name == "EquipmentPremiumAdjustmentRisk" || class1.Name == "EquipmentPremiumAdjustmentManaged" || class1.Name == "EquipmentPremiumAdjustmentShared" || class1.Name == "EquipmentPremiumAdjustmentCapped" || class1.Name == "EquipmentPremiumAdjustmentDirectedService") &&(typeof(EquipmentPremiumAdjustmentViewModelBase) .GetMember(column.Member).FirstOrDefault() != null))) {#>override <#}#><#=code.Format(column.Type)#> <#=column.Member#> { get { <# if (!column.IsPrimaryKey && !column.IsReadOnly){#> _ind<#=column.Member#>.OnGet(); <# } if (column.CanBeNull){#> if (this.IsDisposed || this.DataContext == null || this.DataContext.IsDisposed) return null; <# } if(class1.Name == "ErrorLog" || class1.Name.StartsWith("vw") ) {#> return _<#= class1.Name#>.<#=column.Member#>; <# } else {#> return <#=column.StorageValue#>; <# }#> } <# if (!column.IsReadOnly && !column.IsPrimaryKey) { #> set { if (<#=column.StorageValue#> != value) { _ind<#=column.Member#>.OnSet(); <#=column.StorageValue#> = value; OnPropertyChanged("<#=column.Member#>"); } } <# }#> } <# }#> #endregion #region Column Validation Warnings' Registration public override List<PropertyRuleBase<ViewModelBase>> PropertiesWarnings { get {return _propertiesWarnings;} protected set {_propertiesWarnings = value;} } protected override void AddAllPropertyWarningsDefinitions(<#=entityBase#> obj) { if (_propertiesWarnings == null) { _propertiesWarnings = new List<PropertyRuleBase<ViewModelBase>>(); } if (_propertiesWarnings.Count == 0) // checking this condition twice to prevent dead locks. { lock (obj) { if (_propertiesWarnings.Count == 0) { // adding properties' warnings collections <# foreach(Column column in class1.Columns) {#> <#=column.Member#>PropertyWarnings = new PropertyRuleBase<ViewModelBase> { PropertyName = "<#=column.Member#>" }; _propertiesWarnings.Add(<#=column.Member#>PropertyWarnings); <# } #> AddPropertyWarnings(); } } } } #endregion #region Column Validation Rules' Registration public override List<PropertyRuleBase<ViewModelBase>> PropertiesRules { get {return _propertiesRules;} protected set {_propertiesRules = value;} } protected override void AddAllPropertyRulesDefinitions(<#=entityBase#> obj) { if (_propertiesRules == null) { _propertiesRules = new List<PropertyRuleBase<ViewModelBase>>(); } if (_propertiesRules.Count == 0) // checking this condition twice to prevent dead locks. { lock (obj) { if (_propertiesRules.Count == 0) { // adding properties' rules collections <# foreach(Column column in class1.Columns) {#> <#=column.Member#>PropertyRules = new PropertyRuleBase<ViewModelBase> { PropertyName = "<#=column.Member#>" }; _propertiesRules.Add(<#=column.Member#>PropertyRules); <# } #> AddPropertyRules(); } } } } #endregion #region Methods protected override bool HasChanged() { return ( <# int columnIndex = 0; foreach(Column column in class1.Columns){ if(columnIndex!=0){#> || <# }#> <#= column.Member #> != _<#= class1.Name #>.<#= column.Member #> <# columnIndex++; }#> || base.HasChanged() ); } <# if(class1.Name != "ErrorLog" && !class1.Name.StartsWith("vw") ) {#> private void OnSaveEvent(object sender, EventArgs e) { _indAudit.OnSet(); } <# }#> #endregion #region Commands #region ReviewAuditCommand <# if(class1.Name != "ErrorLog" && !class1.Name.StartsWith("vw")) {#> protected override void DoReviewAuditRefresh() { <# foreach(Column column in class1.Columns) { if (!column.IsPrimaryKey && !column.IsReadOnly) {#> <#=column.Member#> = (AuditRecord as <#=class1.Name#>Audit).<#=column.Member#><# if(column.Type.FullName == "System.Int32" || column.Type.FullName == "System.Boolean" || column.Type.FullName == "System.DateTime" || column.Type.FullName == "System.Decimal" || column.Type.FullName == "System.Byte" ){#>.Value<#}#>; <# } }#> } <# }#> #endregion #region CurrentRecordCommand <# if(class1.Name != "ErrorLog" && !class1.Name.StartsWith("vw")) {#> protected override void DoCurrentRecordRefresh() { base.DoCurrentRecordRefresh(); <# foreach(Column column in class1.Columns) { if (!column.IsPrimaryKey && !column.IsReadOnly) {#> <#=column.Member#> = _<#= class1.Name #>.<#= column.Member #>; <# } }#> } <# }#> #endregion #region CancelCommand protected override void DoCancel() { base.DoCancel(); // restore values for all properties <# foreach(Column column in class1.Columns){ if (!column.IsReadOnly && !column.IsPrimaryKey){#> <#= column.Member #> = _<#= class1.Name #>.<#= column.Member #>; <# } }#> } #endregion #region SaveCommand protected override void DoSave() { IsUpdating = true; // set values for all properties <# foreach(Column column in class1.Columns){ if (!column.IsReadOnly && !column.IsPrimaryKey && class1.Associations.Where(association => association.ThisKey.FirstOrDefault().Name == column.Name).Count()==0){#> _<#= class1.Name #>.<#= column.Member #> = <#if(column.Type.FullName == "System.String") {#> ((<#=column.Member#> == null)?String.Empty : <#= column.Member #>);<#} else {#> <#=column.Member #>;<# } #> <# } }#> bool isNew = this.IsNew; <# foreach(Association association in class1.Associations){ if(!association.IsMany && association.IsForeignKey){#> if(<#=association.ThisKey.FirstOrDefault().Member#>!= _<#= class1.Name #>.<#= association.ThisKey.FirstOrDefault().Member #>) { if(isNew) { _<#= class1.Name #>.<#= association.ThisKey.FirstOrDefault().Member #> = _<#=association.ThisKey.FirstOrDefault().Member#>; } else { _<#= class1.Name #>.<#= association.Member#> = DataContext.<#= association.Type.Table.Member#>.Where(entity => entity.<#= association.OtherKey.FirstOrDefault().Member#> == <#=association.ThisKey.FirstOrDefault().Member#>).FirstOrDefault(); } } <# } }#> base.DoSave(); IsUpdating = false; } protected override void DoErase() { IsUpdating = true; // set values for all properties <# foreach(Column column in class1.Columns){ if (!column.IsReadOnly && !column.IsPrimaryKey){#> _<#= class1.Name #>.<#= column.Member #> = default(<#=code.Format(column.Type)#>); <# } }#> IsUpdating = false; } #endregion #region RefreshCommand protected override void DoRefresh(bool fromDatabase) { if (fromDatabase) { //refresh values from database. DataContext.Refresh(RefreshMode.OverwriteCurrentValues, DataEntity); } <# foreach(Column column in class1.Columns){ if(!column.IsReadOnly && !column.IsPrimaryKey){#> <#=column.Member#> = _<#= class1.Name#>.<#=column.Member#>; <# } if(column.IsReadOnly && !column.IsPrimaryKey) {#> <#=column.Storage#> = _<#= class1.Name #>.<#= column.Member #>; <# } }#> <# foreach(Association association in class1.Associations){ if(!association.IsMany && association.IsForeignKey){#> _<#= class1.Name #>.<#= association.Member#> = DataContext.<#= association.Type.Table.Member#>.Where(entity => entity.<#= association.OtherKey.FirstOrDefault().Member#> == _<#=association.ThisKey.FirstOrDefault().Member#>).FirstOrDefault(); <# } }#> } #endregion #endregion #region Cleanup protected override void Cleanup() { if (_saveHandler != null) { this.SavedEvent -= _saveHandler; _saveHandler = null; } if (_propertyChangedHandler != null && _<#= class1.Name#> != null) { this._<#= class1.Name #>.PropertyChanged -= _propertyChangedHandler; _propertyChangedHandler = null; } //_<#= class1.Name #> = null; base.Cleanup(); } #endregion //Cleanup <# } if (class1.Associations.Count > 0) { #> #region Associations <# foreach(Association association in class1.Associations) {#> private <# if (association.IsMany) { #>IEnumerable< <#= association.Type.Name#>ViewModel><# } else {#> <#= association.Type.Name#>ViewModel<#}#> _<#=association.Member#>; //private Independent _ind<#=association.Member#> = new Independent(); <#=code.Format(association.MemberAttributes)#><#if (association.IsMany) { #>IEnumerable<<#=association.Type.Name#>ViewModel><# } else {#> <#= association.Type.Name#>ViewModel<#} #> <#=association.Member#> { get { if (this.IsDisposed || this.DataContext == null || this.DataContext.IsDisposed) return null; <# if (class1.Name.StartsWith("vw") || class1.Name == "ErrorLog") {#> if (_<#=association.Member#>==null || this.<#=association.ThisKey.FirstOrDefault().Name#> != this._<#=class1.Name#>.<#=association.ThisKey.FirstOrDefault().Name#> <# if (!association.IsMany){#> || _<#=association.Member#>.<#=association.OtherKey.FirstOrDefault().Name#> != this._<#=class1.Name#>.<#=association.ThisKey.FirstOrDefault().Name#><#}#>) { <# if(association.IsMany) {#> List<<#= association.Type.Name#>ViewModel> list = new List<<#= association.Type.Name#>ViewModel>(); foreach(<#=association.Type.Name#> entity in DataContext.<#=association.Type.Table.Member#>.Where(t=> t.<#=association.OtherKey.FirstOrDefault().Name#>==this.<#=association.ThisKey.FirstOrDefault().Name#>)<#if(!association.IsMany){#>.FirstOrDefault()<#}else{#><#}#>) { list.Add(<#=association.Type.Name#>ViewModel.Create(entity, DataContext.<#=association.Type.Table.Member#>)); } _<#=association.Member#> = list.AsEnumerable(); <# } else {#> _<#=association.Member#> = <#=association.Type.Name#>ViewModel.Create(DataContext.<#=association.Type.Table.Member#>.Where(t=> t.<#=association.OtherKey.FirstOrDefault().Name#>==this.<#=association.ThisKey.FirstOrDefault().Name#>)<#if(!association.IsMany){#>.FirstOrDefault()<#}else{#><#}#>,DataContext.<#=association.Type.Table.Member#>); <# } #> } return _<#=association.Member#>; <# } else {#> //_ind<#=association.Member#>.OnGet(); //*(HistoryRecord as <#=class1.Name#>History).<#=association.Member#>*/ if (_<#=association.Member#>==null || this.<#=association.ThisKey.FirstOrDefault().Name#> != this._<#=class1.Name#>.<#=association.ThisKey.FirstOrDefault().Name#> <# if (!association.IsMany){#> || _<#=association.Member#>.<#=association.OtherKey.FirstOrDefault().Name#> != this._<#=class1.Name#>.<#=association.ThisKey.FirstOrDefault().Name#><#}#>) { <# if(association.IsMany) {#> List<<#= association.Type.Name#>ViewModel> list = new List<<#= association.Type.Name#>ViewModel>(); foreach(<#=association.Type.Name#> entity in DataContext.<#=association.Type.Table.Member#>.Where(t=> t.<#=association.OtherKey.FirstOrDefault().Name#>==this.<#=association.ThisKey.FirstOrDefault().Name#>)<#if(!association.IsMany){#>.FirstOrDefault()<#}else{#><#}#>) { list.Add(<#=association.Type.Name#>ViewModel.Create(entity, DataContext.<#=association.Type.Table.Member#>)); } _<#=association.Member#> = list.AsEnumerable(); <# } else {#> _<#=association.Member#> = <#=association.Member#>ViewModel.Create(DataContext.<#=association.Type.Table.Member#>.Where(t=> t.<#=association.OtherKey.FirstOrDefault().Name#>==this.<#=association.ThisKey.FirstOrDefault().Name#>)<#if(!association.IsMany){#>.FirstOrDefault()<#}else{#><#}#>,DataContext.<#=association.Type.Table.Member#>); <# } #> } <# if(!association.Member.StartsWith("vw") && association.Member != "ErrorLog") {#> <#=class1.Name#>Audit auditRecord = (AuditRecord as <#=class1.Name#>Audit); if (auditRecord != null) { var key = auditRecord.<#=association.ThisKey.FirstOrDefault().Name#><# if(association.ThisKey.FirstOrDefault().Type.FullName == "System.Int32" || association.ThisKey.FirstOrDefault().Type.FullName == "System.Boolean" || association.ThisKey.FirstOrDefault().Type.FullName == "System.DateTime" || association.ThisKey.FirstOrDefault().Type.FullName == "System.Decimal" || association.ThisKey.FirstOrDefault().Type.FullName == "System.Byte" ){#>.Value<#}#>; <# if (association.IsMany) {#> //foreach(<#=association.Type.Name#>ViewModel vm in _<#=association.Member#>) //{ // DataModelAuditBase ar = ShmmpAudit.<#=association.Type.Name#>Audits // .Where(t=>t.<#=association.OtherKey.FirstOrDefault().Name#> == key // && t.DateStamp <= auditRecord.DateStamp).FirstOrDefault(); // if (ar != null) // vm.AuditRecord = ar; //} List<<#= association.Type.Name#>ViewModel> list = new List<<#= association.Type.Name#>ViewModel>(); foreach(<#=association.Type.Name#> entity in DataContext.<#=association.Type.Table.Member#>.Where(t=> t.<#=association.OtherKey.FirstOrDefault().Name#>==key)<#if(!association.IsMany){#>.FirstOrDefault()<#}else{#><#}#>) { list.Add(<#=association.Type.Name#>ViewModel.Create(entity, DataContext.<#=association.Type.Table.Member#>)); } _<#=association.Member#> = list.AsEnumerable(); <# } else {#> DataModelAuditBase ar = ShmmpAudit.<#=association.Type.Name#>Audits .Where(t=>t.<#=association.OtherKey.FirstOrDefault().Name#> == key && ((t.TransactionId == auditRecord.TransactionId && (t.DateStamp - auditRecord.DateStamp).Minutes < 60 ) || (t.DateStamp >= auditRecord.DateStamp)) ).FirstOrDefault(); if (ar != null) { if(_<#=association.Member#>.AuditRecord != ar) { _<#=association.Member#>.AuditRecord = ar; } } else _<#=association.Member#> = <#=association.Member#>ViewModel.Create( DataContext.<#=association.Type.Table.Member#>. Where(t=> t.<#=association.OtherKey.FirstOrDefault().Name#>==key)<#if(!association.IsMany){#>.FirstOrDefault()<#}else{#><#}#>,DataContext.<#=association.Type.Table.Member#>); <# }#> } <# }#> return _<#=association.Member#>; <# }#> } } <# }#> #endregion <# } #> } <# if (!String.IsNullOrEmpty(data.EntityNamespace)) {#> } <# } manager.EndBlock(); } } manager.StartFooter();#> #pragma warning restore 1591 <#manager.EndBlock(); manager.Process(options.FilePerEntity);#>
These templates are very similar to ASP, but reading such templates is very hard. So to read them easily there is a special and FREE markup tool developed by Tangible Engineering and a nice intro article written by Oleg Sych.
- For collections I use ObservableCollections thus disabling UpdateControls for them and preventing creation of millions of event handlers and events for large collections.
- For every object which gets into the view or somehow selected by a user a view model is generated. This dramatically improves performance of UpdateControls while still gives me a no worry approach for my PropertyChange events. Each view model may have custom validation logic or anything else since all classes are generated as partial.
- Having business logic in model views also helps working around some limitations of LINQ to SQL 3.5. Such as inability to detach entities, undo operations or working in concurrent scenarios with multiple threads.
Having being able to find solution to all these problems I realized that such approach brings many additional benefits to the table. In simple words in my application:
- all lists are ObservableCollections of data models. Since data models are also partial classes, they can be easily extended with additional functionality.
- users make changes only to view models. Thus having the ability to roll back changes if database errors occur or having the ability to change multiple objects at the same time. Both of these things are impossible with current LINQ to SQL, since all the changes are made directly to the model and there is no way to detach any objects. The whole model needs to be disposed. While disposing the whole model means all change tracking that is built into LINQ to SQL classes is useless. Now in my model I get to enjoy the benefits of change tracking, and the ability to roll back changes. When objects need to be saved (persisted) to a data store, it is done in a separate instance of database DataContext. If errors occur I throw away such temporary data context, since I can’t detach entity from the model. And notify users about the error. If everything is fine, the new entity gets attached to the current model, or changed entity is refreshed from the data store.
- another benefit is data mining. Since I am using only one data context internally I am able to do joins between multiple lists and run queries of different complexities. Current LINQ to SQL doesn’t allow queries to overlap collections from different instances of the same data context.
So this was another not so easy, but still ISolvable<TProblem>.
Happy Coding and have a wonderful New Year!
I believe the samples used to be online but are now only available in the zip file. You might look into the I Notify Property Changed / IE ditable Object support provided by the Post Sharp samples.
ReplyDeleteHey Ivan, great article. You can also check out PropertyChangedPropagator (based on INotifyPropertyChanged) which helps to build dependency graphs in more natural "Excel-like" way i.e. independent properties shouldn't know who depend on them:
ReplyDeletehttp://www.codeproject.com/Articles/775831/INotifyPropertyChanged-propagator