Search code examples
c#wpfdata-annotationsef-database-firstbuddy-class

DataAnnotation in WPF with Database first approach - how to move data annotations to buddy class incl. IsValid function


As it is shortly described in the topic: How to move all DataAnnotations from model to MetaData model in order not to wipe it out when updating edmx?
In another words I would like to have data annotation secure and not get rid of with each update of edmx and I would have in dataannotation an option to check if all data annotations requirements are fulfiled (IsValid method) to use it in CanExecute method of RelayCommand.

I have a class as follows:

public partial class Customer : IDataErrorInfo
{
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
    public int ID{ get; set; }

    [Required(ErrorMessage = "Field required")]
    public string Name{ get; set; }

    [Required(ErrorMessage = "Field required")]
    public string LastName{ get; set; }

    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
    public virtual ICollection<tblKontrahent> tblKontrahent { get; set; }


    #region Validation
    public bool IsValid { get; set; } 

    public string Error { get { return null; } }

    public string this[string columnName]
    {
        get
        {
            Validation();
            return InputValidation<Customer >.Validate(this, columnName);
        }
    }

    public ICollection<string> AllErrors()
    {
        return InputValidation<Customer >.Validate(this);
    }

    private void Validation()
    {
        ICollection<string> allErrors = AllErrors();
        if (allErrors.Count == 0)
            IsValid = true;
        else
            IsValid = false;
    }
    #endregion


    #region Shallow copy
    public Customer ShallowCopy()
    {
        return (Customer )this.MemberwiseClone();
    }
    #endregion
}

How to move it from Model to MetaDataModel with the annotations and IsValid function. It would be great if ShallowCopy method could be also moved.

Thank you VERY MUCH for any suggestions!.


Solution

  • For most substantial apps I keep the EF classes totally separate. I copy properties from entity framework to a viewmodel which is self tracking.

    For smaller apps I used to avoid doing this.

    You can see an approach used in this:

    https://gallery.technet.microsoft.com/scriptcenter/WPF-Entity-Framework-MVVM-78cdc204

    That uses INotifyDataErrorInfo and you'll find IsValid in BaseEntity. Which is a pretty complicated class but re-usable.

    You could probably refactor shallowcopy into BaseEntity. Easily if you're ok casting wherever you use it.

    Annotations are in separate buddy classes. You can see examples in Customer.metadata.cs and Product.metadata.cs These are partial classes which add inheritance to BaseEntity to the entity classes. Hence the EF class Customer inherits BaseEntity.

    An example:

    using DataAnnotationsExtensions;
    
    namespace wpf_EntityFramework.EntityData
    {
    [MetadataTypeAttribute(typeof(Product.ProductMetadata))]
    public partial class Product : BaseEntity, IEntityWithId
    {
        public void MetaSetUp()
        {
            // In wpf you need to explicitly state the metadata file.
            // Maybe this will be improved in future versions of EF.
            TypeDescriptor.AddProviderTransparent(
                new AssociatedMetadataTypeTypeDescriptionProvider(typeof(Product),
                typeof(ProductMetadata)),
                typeof(Product));
        }
        internal sealed class ProductMetadata
        {
            // Some of these datannotations rely on dataAnnotationsExtensions ( Nuget package )
            [Required(ErrorMessage="Product Short Name is required")]
            public string ProductShortName { get; set; }
    
            [Required(ErrorMessage = "Product Weight is required")]
            [Min(0.01, ErrorMessage = "Minimum weight is 0.01")]
            [Max(70.00, ErrorMessage = "We don't sell anything weighing more than 70Kg")]
            public Nullable<decimal> Weight { get; set; }
    
            [Required(ErrorMessage = "Bar Code is required")]
            [RegularExpression(@"[0-9]{11}$", ErrorMessage="Bar codes must be 11 digits")]
            public string BarCode { get; set; }
    
            [Required(ErrorMessage = "Price per product is required")]
            [Range(0,200, ErrorMessage="Price must be 0 - £200") ]
            public Nullable<decimal> PricePer { get; set; }
            private ProductMetadata()
            { }
        }
    }
    

    }

    As the comment in that says.

    You need to call that Metasetup on each instance. Unless something changed in the last few years. The buddy class isn't just picked up like with MVC.

    The sample also feeds back conversion failures from the UI.

    See the template in Dictionary1.

    <ControlTemplate x:Key="EditPopUp" TargetType="ContentControl">
        <ControlTemplate.Resources>
            <Style TargetType="{x:Type TextBox}" BasedOn="{StaticResource ErrorToolTip}">
                <Setter Property="HorizontalAlignment" Value="Stretch"/>
            </Style>
        </ControlTemplate.Resources>
        <Grid Visibility="{Binding IsInEditMode, Converter={StaticResource BooleanToVisibilityConverter}}" 
               Width="{Binding ElementName=dg, Path=ActualWidth}"
               Height="{Binding ElementName=dg, Path=ActualHeight}"
               >
                    <i:Interaction.Triggers>
                        <local:RoutedEventTrigger RoutedEvent="{x:Static Validation.ErrorEvent}">
                            <e2c:EventToCommand
                                                    Command="{Binding EditVM.TheEntity.ConversionErrorCommand, Mode=OneWay}"
                                                    EventArgsConverter="{StaticResource BindingErrorEventArgsConverter}"
                                                    PassEventArgsToCommand="True" />
                        </local:RoutedEventTrigger>
                        <local:RoutedEventTrigger RoutedEvent="{x:Static Binding.SourceUpdatedEvent}">
                            <e2c:EventToCommand
                                                    Command="{Binding EditVM.TheEntity.SourceUpdatedCommand, Mode=OneWay}"
                                                    EventArgsConverter="{StaticResource BindingSourcePropertyConverter}"
                                                    PassEventArgsToCommand="True" />
                        </local:RoutedEventTrigger>
                    </i:Interaction.Triggers>