Hi am struggling with validation with data annotations in WPF
I have a class in a separate project because we are reusing the class across multiple projects.Several of the fields have data annotations for validation.
the validation class combines the errors returned from data annotations with some custom business rules which are different per client
internal class Validation<T> : ValidationRule where T : class, ICore
{
IUnitofWork unitofWork;
List<Func<T, bool>> CompiledRules = new List<Func<T, bool>>();
List<Rule<T>> Rules = new List<Rule<T>>();
Dictionary<string, List<string>> _errors = new Dictionary<string, List<string>>();
List<Func<Object, bool>> FieldRules = new List<Func<object, bool>>();
public Validation()
{
unitofWork = new UnitOfWork();
CompileRules();
}
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
return IsValid((T)value);
}
public ValidationResult Validate(object value,string PropertyName ,CultureInfo cultureInfo)
{
return IsValid((T)value,PropertyName);
}
void CompileRules()
{
Type t = typeof(T);
List<BusinessRule> rules = unitofWork.Repository<BusinessRule>().GetAll(b => b.Name == t.Name && b.Enabled == true);
foreach (BusinessRule b in rules)
{
Func<T, bool> CompiledRule = CompileRule(b);
CompiledRules.Add(CompiledRule);
Rule<T> rule = new Rule<T>();
rule.CompiledRule = CompiledRule;
rule.ErrorMessage = b.MessageTemplate;
rule.FieldName = b.Field;
Rules.Add(rule);
}
}
ValidationResult IsValid(T Value)
{
bool valid = true;
_errors.Clear();
if (CompiledRules.Count > 0 || Value.Errors.Count > 0)
{
//valid = CompiledRules.All(rule => rule(Value));
foreach (Rule<T> r in Rules)
{
bool isValid = r.CompiledRule(Value);
r.PassedRule = isValid;
string field = r.FieldName;
if (!isValid )
{
valid = false;
string ErrorMessage = string.Format("{0} {1}", r.FieldName, r.ErrorMessage);
if (_errors.ContainsKey(field))
_errors[field].Add(ErrorMessage);
else
{
List<string> el = new List<string>();
el.Add(ErrorMessage);
_errors.Add(field, el);
}
}
}
}
ValidateAnnotations(Value,ref valid);
return new ValidationResult(valid, _errors);
}
void ValidateAnnotations(object Value,ref bool Valid)
{
DataAnnotationValidator annotationValidator = new DataAnnotationValidator();
List< System.ComponentModel.DataAnnotations.ValidationResult> results = annotationValidator.ValidateObject(Value);
if (results.Count > 0)
{
Valid = false;
foreach (System.ComponentModel.DataAnnotations.ValidationResult r in results)
{
if (_errors.ContainsKey(r.MemberNames.First()))
{
_errors[r.MemberNames.First()].Add(r.ErrorMessage);
}
else
{
List<string> propErrors = new List<string>();
propErrors.Add(r.ErrorMessage);
_errors.Add(r.MemberNames.First(), propErrors);
}
}
}
}
void ValidateAnnotations(object Value,string PropertyName, ref bool Valid)
{
DataAnnotationValidator annotationValidator = new DataAnnotationValidator();
List<System.ComponentModel.DataAnnotations.ValidationResult> results = annotationValidator.ValidateObject(Value, PropertyName);
if (results.Count > 0)
{
Valid = false;
foreach (System.ComponentModel.DataAnnotations.ValidationResult r in results)
{
if (_errors.ContainsKey(r.MemberNames.First()))
{
_errors[r.MemberNames.First()].Add(r.ErrorMessage);
}
else
{
List<string> propErrors = new List<string>();
propErrors.Add(r.ErrorMessage);
_errors.Add(r.MemberNames.First(), propErrors);
}
}
}
}
ValidationResult IsValid(T Value, string PropertyName)
{
_errors.Remove(PropertyName);
bool valid = true;
if (CompiledRules.Count > 0)
{
//valid = CompiledRules.All(rule => rule(Value));
foreach (Rule<T> r in Rules.Where(b=>b.FieldName == PropertyName))
{
bool isValid = r.CompiledRule(Value);
r.PassedRule = isValid;
string field = r.FieldName;
// string field = "SelectedRow." + r.FieldName;
if (!isValid)
{
valid = false;
string ErrorMessage = string.Format("{0} {1}", r.FieldName, r.ErrorMessage);
if (_errors.ContainsKey(field))
_errors[field].Add(ErrorMessage);
else
{
List<string> el = new List<string>();
el.Add(ErrorMessage);
_errors.Add(field, el);
}
}
}
}
ValidateAnnotations(Value,PropertyName, ref valid);
return new ValidationResult(valid, _errors);
}
public Func<T, bool> CompileRule(BusinessRule r)
{
var paramT = Expression.Parameter(typeof(T));
Expression expression = BuildExpr(r, paramT);
return Expression.Lambda<Func<T, bool>>(expression, paramT).Compile();
}
static Expression BuildExpr(BusinessRule r, ParameterExpression param)
{
var left = MemberExpression.Property(param, r.Field);
var tProp = typeof(T).GetProperty(r.Field).PropertyType;
ExpressionType tBinary;
// is the operator a known .NET operator?
if (ExpressionType.TryParse(r.Operator, out tBinary))
{
var right = Expression.Constant(Convert.ChangeType(r.CompareValue, tProp));
// use a binary operation, e.g. 'Equal' -> 'u.Age == 15'
return Expression.MakeBinary(tBinary, left, right);
}
else
{
var method = tProp.GetMethod(r.Operator);
var tParam = method.GetParameters()[0].ParameterType;
var right = Expression.Constant(Convert.ChangeType(r.CompareValue, tParam));
// use a method call, e.g. 'Contains' -> 'u.Tags.Contains(some_tag)'
return Expression.Call(left, method, right);
}
}
}
internal class Rule<T> where T : class, ICore
{
public string FieldName
{ get;set; }
public Func<T, bool> CompiledRule
{ get; set; }
public Func<object,bool> RevisedRule
{ get; set; }
public string ErrorMessage
{ get; set; }
public bool PassedRule
{ get; set; }
}
internal class Error
{
public string ErrorMessage { get; set; }
public string FieldName { get; set; }
}
The is working as the _errors list has the field names and any errors associated with them.
once we have these we loop through and raise the onPropertyErrorsChangedEvent
internal void SetErrorDetails(ValidationResult Result)
{
propErrors = (Dictionary<string, List<string>>)Result.ErrorContent;
foreach (string key in propErrors.Keys)
{
OnPropertyErrorsChanged(key);
}
}
on my view the 2 fields are
<TextBox Canvas.Left="138"
Canvas.Top="75"
FontFamily="Verdana"
HorizontalAlignment="Left"
Height="20"
Text="{Binding OrganizationName,ValidatesOnDataErrors=True,NotifyOnValidationError=True,ValidatesOnNotifyDataErrors=True,ValidatesOnExceptions=True}" VerticalAlignment="Top" Width="137"
Validation.ErrorTemplate="{StaticResource ValidationTemplate }"
Style="{StaticResource TextErrorStyle}"/>
<TextBox Canvas.Left="138"
Canvas.Top="225"
FontFamily="Verdana"
HorizontalAlignment="Left"
Height="20"
Text="{Binding SelectedRow.Postcode,ValidatesOnDataErrors=True,ValidatesOnNotifyDataErrors=True,ValidatesOnExceptions=True}"
VerticalAlignment="Top"
Width="137"
Validation.ErrorTemplate="{StaticResource ValidationTemplate }"
Style="{StaticResource TextErrorStyle}" />
I am encountering 2 problems i am encountering
When bound directly to the selectedrow (Postcode) my data annotations appear when the form is loaded but when bound via a field on my view model (Organisation Name) they do not . We need to bind these to fields on the view model so that the business rules get run as part of validation.
Second problem if i save the form with an invalid entry for the organisation the save stops because it is invalid however i don't get an error notification even though there is an error for the property in the _errors.
I am not sure what i am doing wrong could someone point me in the right direction please?
[Edit]
We use a third party document service to create and show the view
void CreateDocument(object Arg)
{
string title = string.Empty;
if (Arg.ToString().ToLower() == "edit" && SelectedRow !=null)
{
if (SelectedRow.OrganizationName != null)
title = SelectedRow.OrganizationName;
}
else
{
SelectedRow = new Address();
title = "New Address";
}
AddressDetailVM detail = new AddressDetailVM(SelectedRow,this);
Document = iInternal.CreateDocument("AddressDetails",
detail,
title
);
detail.Document = Document;
// Document = iInternal.CreateDocument("AddressDetails", null, this, title);
Document.Show();
}
Related
Im trying to bind one combobox from my class that i linked in the context
i have this in the control
<dxe:ComboBoxEdit Width="100" Margin="5" Name="cboProduct" DisplayMember="Name"
SelectedItem="{Binding Path=DataContext.SelectedProduct, Mode=OneWay, RelativeSource={RelativeSource AncestorType=Window}}"
>
</dxe:ComboBoxEdit>
I filled the Combobox befor from code behind like this
var lsproducts = new List<Product>();
var Products =_licenseService.GetProductList();
Products.ForEach((x) => {
lsproducts.Add(new Product(x.Name, x.ProductId));
});
And im setting the SelectedProduct like this
[DataMember]
public License SelectedLicense {
get { return _SelectedLicense;}
set {
_SelectedLicense = value;
this.NotifyPropertyChanged("SelectedLicense");
}
}
public Product SelectedProduct
{
get
{
return new Product(_SelectedLicense.Product.Name,_SelectedLicense.Product.ProductId);
}
}
this.cboProduct.ItemsSource = lsproducts.ToArray();
in both cases im using the object Product
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using pd.Common.Domain;
namespace LicenceManagerWPF.Views
{
public class Product
{
public string Name { get; set; }
public ProductEnum ProductID { get; set; }
public Product(string ProductName,ProductEnum ID)
{
Name = ProductName;
ProductID = ID;
}
}
i dont know why its not selecting the product when i open the window.
i have another one
[![Shows like this][2]][2]
I dont know why its display the x mark, but when i chouse another licence it updated the combo selection
<dxe:ComboBoxEdit x:Name="cboActivationMode" Grid.Row="0" Grid.Column="1" HorizontalAlignment="Left" Width="100" Style="{StaticResource TabMargin}"
SelectedItem="{Binding Path=DataContext.SelectedLicense.ActivationMode, RelativeSource={RelativeSource AncestorType=Window}}"
/>
The second one its only a couple of enums values that fill like this.
cboActivationMode.Items.Add(
new DevExpress.Xpf.Editors.ComboBoxEditItem()
{ Content = Enum.GetName(typeof(ActivationMode), ActivationMode.Online), Tag = ActivationMode.Online });
How can i bind the values to a combobox?
Regards
cboActivationMode.Items.Add(
new DevExpress.Xpf.Editors.ComboBoxEditItem()
{Content = Enum.GetName(typeof(ActivationMode), ActivationMode.Offline),Tag = ActivationMode.Offline});
I tryed to do this
public partial class LicenseDetail : UserControl
{
private readonly LicenseService _licenseService;
public LicenseDetail()
{
InitializeComponent();
_licenseService = new LicenseService();
FillCombos();
}
private void FillCombos()
{
FillProducts();
FillActivationMode();
//var str = Enum.GetName(typeof(ProductEnum), ProductEnum.CDSAddIn);
//this.cboProduct.ItemsSource = new string[] { str };
}
private void FillProducts()
{
var Products = _licenseService.GetProductList();
cboProduct.ItemsSource = Products;
}
The product list is a list of Iproduct (interface), i dont get i why they made it like this but each product is diferent they implement the same baseclass and the interface
[DataContract]
public class ProductList : List<IProduct>
{
public bool Contains(ProductEnum productId)
{
return this.Any(x => x.ProductId == productId);
}
public IProduct GetProduct(ProductEnum productId)
{
return this.FirstOrDefault(x => x.ProductId == productId);
}
public void Remove(ProductEnum productId)
{
Remove(GetProduct(productId));
}
}
I changed the combo to bind like this
SelectedItem="{Binding Path=DataContext.MyProduct, RelativeSource={RelativeSource AncestorType=Window}}"
I create the property in the class like this
public IProduct MyProduct
{
get { return _MyProduct; }
set
{
_MyProduct = value;
this.NotifyPropertyChanged("MyProduct");
}
}
And assaing like this
_CustomerLicense.MyProduct = SelectedLicense.Product;
this is how the list of products is filled
public IProduct GetProduct(ProductEnum productId)
{
IProduct product = null;
var connection = GetConnection();
try
{
var sql = string.Format(Resources.GetProduct, (int)productId);
var cmd = new SqlCommand(sql, connection) { CommandType = CommandType.Text, Transaction = _transaction };
using (var rdr = new NullableDataReader(cmd.ExecuteReader()))
while (rdr.Read())
{
var productName = rdr.GetString(0);
var featureId = rdr.GetInt32(1);
var featureDesc = rdr.GetString(2);
if (product == null)
{
switch (productId)
{
case ProductEnum.PDCalc:
product = new PDCalcProduct(productId, productName);
break;
case ProductEnum.PDMaint:
product = new PDMaintProduct(productId, productName);
break;
case ProductEnum.PBDynamics:
product = new PBDynamicsProduct(productId, productName);
break;
case ProductEnum.CDSAddIn:
product = new CDSAddInProduct(productId, productName);
break;
}
}
if (product != null)
product.Features.Add(new Feature((FeatureEnum)featureId, featureDesc));
}
}
finally
{
CloseConnection(connection);
}
return product;
}
without any luck.
Regards
The SelectedProduct property should have a public setter for you to be able to set it to the currently selected value in the ComboBox:
private Product _selectedProduct;
public Product SelectedProduct
{
get { return _selectedProduct; }
set
{
_selectedProduct = value;
this.NotifyPropertyChanged("SelectedProduct");
}
}
And for the intial value to be selected, you either need to set it to a Product object that is actually in the ItemsSource (lsproducts):
viewModel.SelectedProduct = lsproducts.FirstOrDefault(x => x.Name == _SelectedLicense.Product.Name && x.ProductID == _SelectedLicense.Product.ProductId);
Or you will have to override the Equals method of your Product class:
public class Product
{
public string Name { get; set; }
public ProductEnum ProductID { get; set; }
public Product(string ProductName, ProductEnum ID)
{
Name = ProductName;
ProductID = ID;
}
public override bool Equals(object obj)
{
Product other = obj as Product;
return other != null && Name == other.Name && ProductID == other.ProductID;
}
}
I fixed like this:
I created the method :
private void GetProducts()
{
var Products = new LicenseService().GetProductList();
Products.ForEach((x) =>
{
lsproducts.Add(new Product(x.Name, x.ProductId));
});
//this.cboProduct.ItemsSource = lsproducts.ToArray();
}
Then i attached to the load of the main windows, where all the controls are
public frmCustomerLicense(CustomerLicenses cl)
{
InitializeComponent();
GetProducts();
_CustomerLicense = cl;
grdLicenses.grdLicences.SelectedItemChanged += GridRowSelected;
}
Then when one of the licenses is selected i set all the bindings
var Record = (DataRowView)grdLicenses.grdLicences.SelectedItem;
var SelectedLicense = (License)Record["License"];
var list = new LicenseService().GetActivityLog(SelectedLicense.SerialNumber)
.OrderByDescending(x => x.ActivityDate)
.ToList();
_CustomerLicense.ActivityLog = list;
_CustomerLicense.Features = new LicenseService().GetFeatures(SelectedLicense.Product.ProductId);
_CustomerLicense.Products = lsproducts;
_CustomerLicense.HasExpDate = SelectedLicense.HasExpirationDate;
//_CustomerLicense.SetLog(list);
_CustomerLicense.SelectedLicense = SelectedLicense;
//_CustomerLicense.SelectedMaintenance = SelectedLicense.Product.ProductId == ProductEnum.PDMaint ? true : false;
_CustomerLicense.SelectedProduct = lsproducts.FirstOrDefault((x) => x.ProductID == SelectedLicense.Product.ProductId);
And in my ViewClass i added this
[DataMember]
public Product SelectedProduct
{
get { return _SelectedProduct; }
set
{
_SelectedProduct = value;
this.NotifyPropertyChanged("SelectedProduct");
}
}
[DataMember]
public List<Product> Products
{
get { return _Products; }
set { _Products = value;
this.NotifyPropertyChanged("Products");
}
}
So, i set the combobox
<dxe:ComboBoxEdit Width="180" Margin="5" Name="cboProduct" DisplayMember="Name"
ItemsSource="{Binding Path=DataContext.Products, RelativeSource={RelativeSource AncestorType=Window}}"
SelectedItem="{Binding Path=DataContext.SelectedProduct, RelativeSource={RelativeSource AncestorType=Window}}"
>
</dxe:ComboBoxEdit>
Doing this works, thanks for your help mm8
as the topic suggests I wan't to modify the Content of the CollectionEditorPicker. This control is used to open the floating Window for the List of nested Properties.
Unfortunally the RadPropertyGrid don't show any Information about the collection in the Field.
How can I set some value in there? For example a placeholder like "Click here to open the collection" or "xx Items" or "Item 1, Item 2, Item 3..." so see some preview or Information about the field.
I've tried it with a template Selector, but if I'm doing so, the opened Popup is not resizable anymore. Also it looses some Information which are in the default CollectionEditorPicker.
Can you help me?
Below a minimal working Example.
The XAML:
<Window x:Class="TelerikPropertyGridTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation"
xmlns:model="clr-namespace:TelerikPropertyGridTest.Model"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.Resources>
<model:TemplateSelector x:Key="RadPropertyListTemplateSelector">
<!-- Not Working -->
<model:TemplateSelector.CollectionsDataTemplate>
<DataTemplate>
<telerik:RadDropDownButton Content="Test">
<telerik:RadDropDownButton.DropDownContent>
<telerik:CollectionEditor telerik:AutoBindBehavior.UpdateBindingOnElementLoaded="Source"
></telerik:CollectionEditor>
</telerik:RadDropDownButton.DropDownContent>
</telerik:RadDropDownButton>
</DataTemplate>
</model:TemplateSelector.CollectionsDataTemplate>
<model:TemplateSelector.FloatNumberTemplate>
<DataTemplate>
<telerik:RadNumericUpDown telerik:AutoBindBehavior.UpdateBindingOnElementLoaded="Value" />
</DataTemplate>
</model:TemplateSelector.FloatNumberTemplate>
<model:TemplateSelector.IntNumberTemplate>
<DataTemplate>
<telerik:RadNumericUpDown telerik:AutoBindBehavior.UpdateBindingOnElementLoaded="Value"
NumberDecimalDigits="0" />
</DataTemplate>
</model:TemplateSelector.IntNumberTemplate>
</model:TemplateSelector>
</Grid.Resources>
<telerik:RadPropertyGrid Item="{Binding ObjectToBind}"
AutoGeneratingPropertyDefinition="RadPropertyGrid_OnAutoGeneratingPropertyDefinition"
EditorTemplateSelector="{StaticResource RadPropertyListTemplateSelector}">
</telerik:RadPropertyGrid>
</Grid>
</Window>
The ViewModel (generates a Random Object for testing)
public class MainWindowViewModel : BindableBase
{
private readonly Random _random = new Random();
private IExampleInterface _objectToBind;
public MainWindowViewModel()
{
this.ObjectToBind = new ExampleImplementation
{
SomeBooleanValue = this._random.Next() % 2 == 1,
SomeDateValue = this.RandomDay(),
SomeIntValue = this._random.Next(),
SomeString = Guid.NewGuid().ToString(),
SubClasses = new List<IExampleInterface>
{
new ExampleImplementation
{
SomeBooleanValue = this._random.Next() % 2 == 1,
SomeDateValue = this.RandomDay(),
SomeIntValue = this._random.Next(),
SomeString = Guid.NewGuid().ToString(),
SubClasses = new List<IExampleInterface>
{
new ExampleImplementation
{
SomeBooleanValue =
this._random.Next() % 2 == 1,
SomeDateValue = this.RandomDay(),
SomeIntValue = this._random.Next(),
SomeString = Guid.NewGuid().ToString()
}
}
}
}
};
}
public IExampleInterface ObjectToBind
{
get { return this._objectToBind; }
set
{
if (this._objectToBind != value)
{
this._objectToBind = value;
this.OnPropertyChanged("ObjectToBind");
}
}
}
private DateTime RandomDay()
{
var start = new DateTime(1995, 1, 1);
var range = (DateTime.Today - start).Days;
return start.AddDays(this._random.Next(range));
}
}
The IExampleInterface (should be later on a real Interface):
public interface IExampleInterface
{
string SomeString { get; set; }
int SomeIntValue { get; set; }
double SomeDouble { get; set; }
IList<IExampleInterface> SubClasses { get; set; }
IList<IExampleInterface> SubClasses2 { get; set; }
bool SomeBooleanValue { get; set; }
DateTime SomeDateValue { get; set; }
SomeEnum SomeEnumValue { get; set; }
}
The ExampleImplementation (should have later on a Real Implementation with additional Properties).
public class ExampleImplementation : BindableBase, IExampleInterface
{
private bool _someBooleanValue;
private DateTime _someDateValue;
private double _someDouble;
private SomeEnum _someEnumValue;
private int _someIntValue;
private string _someString;
private ObservableCollection<IExampleInterface> _subClasses;
private ObservableCollection<IExampleInterface> _subClasses2;
public bool SomeBooleanValue
{
get { return this._someBooleanValue; }
set
{
if (this._someBooleanValue != value)
{
this._someBooleanValue = value;
this.OnPropertyChanged("SomeBooleanValue");
}
}
}
public DateTime SomeDateValue
{
get { return this._someDateValue; }
set
{
if (this._someDateValue != value)
{
this._someDateValue = value;
this.OnPropertyChanged("SomeDateValue");
}
}
}
public double SomeDouble
{
get { return this._someDouble; }
set
{
if (Math.Abs(this._someDouble - value) > 0.01)
{
this._someDouble = value;
this.OnPropertyChanged("SomeDouble");
}
}
}
public SomeEnum SomeEnumValue
{
get { return this._someEnumValue; }
set
{
if (this._someEnumValue != value)
{
this._someEnumValue = value;
this.OnPropertyChanged("SomeEnumValue");
}
}
}
public int SomeIntValue
{
get { return this._someIntValue; }
set
{
if (this._someIntValue != value)
{
this._someIntValue = value;
this.OnPropertyChanged("SomeIntValue");
}
}
}
[Display(Name = #"TestString", GroupName = #"TestGroup", Description = #"TestDescription")]
public string SomeString
{
get { return this._someString; }
set
{
if (this._someString != value)
{
this._someString = value;
this.OnPropertyChanged("SomeString");
}
}
}
[Display(Name = #"Some Subclasses")]
public IList<IExampleInterface> SubClasses
{
get { return this._subClasses; }
set
{
if (!Equals(this._subClasses, value))
{
this._subClasses = new ObservableCollection<IExampleInterface>(value);
this.OnPropertyChanged("SubClasses");
}
}
}
public IList<IExampleInterface> SubClasses2
{
get { return this._subClasses2; }
set
{
if (!Equals(this._subClasses2, value))
{
this._subClasses2 = new ObservableCollection<IExampleInterface>(value);
this.OnPropertyChanged("SubClasses2");
}
}
}
}
And finally the TemplateSelector
public class TemplateSelector : DataTemplateSelector
{
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
var def = item as PropertyDefinition;
if (def == null || def.SourceProperty == null)
{
return base.SelectTemplate(item, container);
}
if (typeof (IEnumerable).IsAssignableFrom(def.SourceProperty.PropertyType) && typeof(string) != def.SourceProperty.PropertyType)
{
return this.CollectionsDataTemplate;
}
if (typeof (double).IsAssignableFrom(def.SourceProperty.PropertyType))
{
return this.FloatNumberTemplate;
}
if (typeof (int).IsAssignableFrom(def.SourceProperty.PropertyType))
{
return this.IntNumberTemplate;
}
return base.SelectTemplate(item, container);
}
public DataTemplate CollectionsDataTemplate { get; set; }
public DataTemplate FloatNumberTemplate { get; set; }
public DataTemplate IntNumberTemplate { get; set; }
}
This is what I expect
The optimal solution would be to get detailed Information in the TextBlock, like Item 1, item 2 etc.
Thank you.
// Edit:
I've figured out the NullReferenceException and got a Demo to work, so that I can modify the text. But the popup is different to the default. Have you an idea to fix it?
I've updated the text and the example.
After wasting a few hours now I figured out a solution to realize this.
I've added a custom behavior to the Collection template. This behavior sets the Header of the CollectionEditor as soon as it's loaded or updated.
Below you can see my modifications:
The Template:
<model:TemplateSelector.CollectionsDataTemplate>
<DataTemplate>
<telerik:RadDropDownButton Content="Click to edit the collection">
<telerik:RadDropDownButton.DropDownContent>
<telerik:CollectionEditor telerik:AutoBindBehavior.UpdateBindingOnElementLoaded="Source"
ResizeGripperVisibility="Visible">
<i:Interaction.Behaviors>
<model:CollectionEditorBehavior />
</i:Interaction.Behaviors>
</telerik:CollectionEditor>
</telerik:RadDropDownButton.DropDownContent>
</telerik:RadDropDownButton>
</DataTemplate>
</model:TemplateSelector.CollectionsDataTemplate>
The behavior:
internal class CollectionEditorBehavior : Behavior<CollectionEditor>
{
protected override void OnAttached()
{
this.AssociatedObject.SourceUpdated += (sender, args) => this.PrepareHeader();
this.AssociatedObject.DataContextChanged += (sender, args) => this.PrepareHeader();
this.AssociatedObject.Loaded += (sender, args) => this.PrepareHeader();
}
private void PrepareHeader()
{
if (this.AssociatedObject == null)
{
// Error Case
return;
}
if (this.AssociatedObject.CollectionView == null ||
this.AssociatedObject.CollectionView.SourceCollection == null)
{
// Source not set
this.AssociatedObject.Header = "Collection";
return;
}
// Get the property from the DataContext to retrieve HeaderInformation
var propInfo = this.AssociatedObject.DataContext
.GetType()
.GetProperties()
.FirstOrDefault(
propertyInfo =>
Equals(propertyInfo.GetValue(this.AssociatedObject.DataContext),
this.AssociatedObject.CollectionView.SourceCollection));
if (propInfo == null)
{
// We didn't got the property Information, using default value
this.AssociatedObject.Header = "Collection";
return;
}
// Getting the DisplayName Attribute
var attr = Attribute.GetCustomAttribute(propInfo,
typeof (DisplayNameAttribute)) as DisplayNameAttribute;
if (attr != null)
{
// We have a DisplayName attribute
this.AssociatedObject.Header = attr.DisplayName;
return;
}
// Alternative: Get the Display Attribute
var attr2 = Attribute.GetCustomAttribute(propInfo,
typeof (DisplayAttribute)) as DisplayAttribute;
if (attr2 != null)
{
// We have the Display Attribute
this.AssociatedObject.Header = attr2.Name;
return;
}
// We have no DisplayAttribute and no DisplayName attribute, set it to the PropertyName
this.AssociatedObject.Header = propInfo.Name;
}
}
I have a situation where a BindingList<> represents a collection of POCOs that have sub-collections of similar nature, Here is a sample code of two such POCOs and their respective lists:
The DirectoryTypePoco
public class DirectoryTypePoco : IBasePoco
{
public DirectoryTypePoco()
{
}
public DirectoryTypePoco(Int16 directoryTypeId, String directoryImplementation, String directoryDescription, DirectoryDefinitionPocoList directoryDefinition)
{
DirectoryTypeId = directoryTypeId;
DirectoryImplementation = directoryImplementation;
DirectoryDescription = directoryDescription;
DirectoryDefinition = directoryDefinition;
}
public Int16 DirectoryTypeId { get; set; }
public String DirectoryImplementation { get; set; }
public String DirectoryDescription { get; set; }
public DirectoryDefinitionPocoList DirectoryDefinition { get; set; }
public object GenerateEntity(GenericRepository repository, params object[] parameters)
{
var lastMaxEntityId = repository.GetQuery<DirectoryType>().Select(select => #select.DirectoryTypeId).DefaultIfEmpty().Max();
var newEntity = new DirectoryType
{
DirectoryTypeId = (short)(lastMaxEntityId + 1),
DirectoryImplementation = this.DirectoryImplementation,
DirectoryDescription = this.DirectoryDescription
};
return newEntity;
}
}
And the BindingList<DirectoryTypePoco>:
public class DirectoryTypePocoList : BindingList<DirectoryTypePoco>
{
public DirectoryTypePocoList()
{
using (var repository = new GenericRepository(new PWRDbContext()))
{
var query = repository.GetQuery<DirectoryType>();
foreach (var r in query)
{
Add(new DirectoryTypePoco(r.DirectoryTypeId, r.DirectoryImplementation, r.DirectoryDescription, new DirectoryDefinitionPocoList(r.DirectoryTypeId)));
}
}
}
public DirectoryTypePocoList(short directoryTypeId)
{
using (var repository = new GenericRepository(new PWRDbContext()))
{
var query = repository.GetQuery<DirectoryType>(where => where.DirectoryTypeId == directoryTypeId);
foreach (var r in query)
{
Add(new DirectoryTypePoco(r.DirectoryTypeId, r.DirectoryImplementation, r.DirectoryDescription, new DirectoryDefinitionPocoList(r.DirectoryTypeId)));
}
}
}
}
The second object: DirectoryDefinitionPoco
public class DirectoryDefinitionPoco : IBasePoco
{
public DirectoryDefinitionPoco()
{
}
public DirectoryDefinitionPoco(Int16 directoryTypeId, Byte parameterId, String parameterName, String parameterValidation, Boolean encryptionRequired, PocoChangeType changeType = PocoChangeType.None)
{
DirectoryTypeId = directoryTypeId;
ParameterId = parameterId;
ParameterName = parameterName;
ParameterDescription = parameterName;
ParameterRequired = false;
ParameterValidation = parameterValidation;
EncryptionRequired = encryptionRequired;
}
public Int16 DirectoryTypeId { get; set; }
public Byte ParameterId { get; set; }
public String ParameterName { get; set; }
public String ParameterDescription { get; set; }
public String ParameterValidation { get; set; }
public Boolean ParameterRequired { get; set; }
public Boolean EncryptionRequired { get; set; }
public object GenerateEntity(GenericRepository repository, params object[] parameters)
{
var masterId = (short) parameters[0];
var lastMaxEntityId = repository.GetQuery<DirectoryDefinition>(where => where.DirectoryTypeId == masterId).Select(select => #select.ParameterId).DefaultIfEmpty().Max();
var newEntity = new DirectoryDefinition
{
DirectoryTypeId = (short)parameters[0],
ParameterId = (byte)(lastMaxEntityId + 1),
ParameterName = this.ParameterName,
ParameterDescription = this.ParameterDescription,
ParameterValidation = this.ParameterValidation,
ParameterRequired = this.ParameterRequired,
EncryptionRequired = this.EncryptionRequired
};
return newEntity;
}
}
And BindingList<DirectoryDefinitionPoco>:
public class DirectoryDefinitionPocoList : BindingList<DirectoryDefinitionPoco>
{
public DirectoryDefinitionPocoList(short directoryTypeId)
{
using (var repository = new GenericRepository(new PWRDbContext()))
{
var query = repository.GetQuery<DirectoryDefinition>(where => where.DirectoryTypeId == directoryTypeId);
foreach (var r in query)
{
Add(new DirectoryDefinitionPoco(r.DirectoryTypeId, r.ParameterId, r.ParameterName, r.ParameterValidation, r.EncryptionRequired));
}
}
}
public List<DirectoryDefinition> GetSourceQuery()
{
List<DirectoryDefinition> result;
using (var repository = new GenericRepository(new PWRDbContext()))
{
result = repository.GetQuery<DirectoryDefinition>().ToList();
}
return result;
}
public List<DirectoryDefinition> GetSourceQuery(short directoryTypeId)
{
List<DirectoryDefinition> result;
using (var repository = new GenericRepository(new PWRDbContext()))
{
result = repository.GetQuery<DirectoryDefinition>(where => where.DirectoryTypeId == directoryTypeId).ToList();
}
return result;
}
}
On the form, I load the data into the grid through a BindingSource component. The child rows are added properly and the data is valid.
Here is the issue: I'm able to add new DirectoryTypePoco but when try to add a DirectoryDefinitionPoco, in the code, the the DirectoryDefinitionPocoobject that I get has a zero for it's parent object. In the above picture, the Test5.dll234 is a DirectoryTypePoco with DirectoryTypeId = 8 and all child under it are ok except the new one I create. What am I suppose to do to make sure I have Master-Child relation in this case?
Ok. It seems that there are two thing I should have noticed in my design.
The individual child Poco needs to know the parent Poco through a reference.
The DevExpress Grid has methods that allow for retrieving the attached data to a parent row while in the child view' particular row.
The first part is straightforwards: add a new property in the child poco of parent poco type.
This however, in my case, doesn't solve my issue as when I visually add a new row on the grid, the default constructor is invoked and it takes no parameters and hence the parent poco reference will remain NULL and the Ids (numeric) will be defaulted to 0
The second point helped fix my issue completely. I was able to conjure up an extension method for the XtraGrid's GridView as follows:
public static class DevExpressGridHelper
{
public static IBasePoco GetPocoFromSelectedRow(this BaseView view)
{
return (IBasePoco)view.GetRow(((GridView)view).FocusedRowHandle);
}
public static IBasePoco GetParentPocoFromSelectedRow(this GridView view)
{
if (view.ParentView !=null)
{
// return (IBasePoco)(view.ParentView).GetRow(((GridView)(view.ParentView)).FocusedRowHandle);
return (IBasePoco)((GridView)view.ParentView).GetFocusedRow();
}
return null;
}
}
And used it as follows:
private void GridMain_Level_1_RowUpdated(object sender, RowObjectEventArgs e)
{
var view = sender as GridView;
if (view == null)
{
return;
}
var pocoObject = e.Row as DirectoryDefinitionPoco;
if (pocoObject == null)
{
return;
}
var parentPocoObject = view.GetParentPocoFromSelectedRow();
if (parentPocoObject == null)
{
return;
}
if (view.IsNewItemRow(e.RowHandle))
{
Create(pocoObject, parentPocoObject);
}
else
{
Update(pocoObject);
}
}
I've just discovered that WPF Markup extension instances are reused in control templates. So each copy of the control template gets the same set of markup extensions.
This doesn't work if you want the extension to maintain some state per control it is attached to. Any idea how to solve this.
Don't store state in the Markup extension. Store it another way. For example.
public abstract class DynamicMarkupExtension : MarkupExtension
{
public class State
{
public object TargetObject { get; set; }
public object TargetProperty { get; set; }
public void UpdateValue(object value)
{
if (TargetObject != null)
{
if (TargetProperty is DependencyProperty)
{
DependencyObject obj = TargetObject as DependencyObject;
DependencyProperty prop = TargetProperty as DependencyProperty;
Action updateAction = () => obj.SetValue(prop, value);
// Check whether the target object can be accessed from the
// current thread, and use Dispatcher.Invoke if it can't
if (obj.CheckAccess())
updateAction();
else
obj.Dispatcher.Invoke(updateAction);
}
else // TargetProperty is PropertyInfo
{
PropertyInfo prop = TargetProperty as PropertyInfo;
prop.SetValue(TargetObject, value, null);
}
}
}
}
public sealed override object ProvideValue(IServiceProvider serviceProvider)
{
IProvideValueTarget target = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
State state = new State();
if (target != null)
{
state.TargetObject = target.TargetObject;
state.TargetProperty = target.TargetProperty;
return ProvideValueInternal(serviceProvider, state);
}
else
{
return null;
}
}
protected abstract object ProvideValueInternal(IServiceProvider serviceProvider, State state);
}
is a base class for handling the type of problem where you need to update the property the markup
extension is attached to at run time. For example a markup extension for binding to ISubject as
<TextBox Text="{Markup:Subscription Path=Excenter, ErrorsPath=Errors}"/>
using the SubscriptionExtension as below. I had had trouble with the code when I used it
within templates but I fixed it so the MarkupExtension did not store state in itself
using ReactiveUI.Ext;
using ReactiveUI.Subjects;
using ReactiveUI.Utils;
using System;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Windows;
using System.Windows.Data;
using System.Windows.Markup;
namespace ReactiveUI.Markup
{
[MarkupExtensionReturnType(typeof(BindingExpression))]
public class SubscriptionExtension : DynamicMarkupExtension
{
[ConstructorArgument("path")]
public PropertyPath Path { get; set; }
[ConstructorArgument("errorsPath")]
public PropertyPath ErrorsPath { get; set; }
public SubscriptionExtension() { }
Maybe<Exception> currentErrorState = Maybe.None<Exception>();
public SubscriptionExtension(PropertyPath path, PropertyPath errorsPath)
{
Path = path;
ErrorsPath = errorsPath;
}
class Proxy : ReactiveObject, IDataErrorInfo, IDisposable
{
string _Value;
public string Value
{
get { return _Value; }
set { this.RaiseAndSetIfChanged(value); }
}
public string Error
{
get { return currentError.Select(e => e.Message).Else(""); }
}
public string this[string columnName]
{
get { return currentError.Select(e => e.Message).Else(""); }
}
public IObservable<Maybe<Exception>> Errors { get; set; }
public Maybe<Exception> currentError = Maybe.None<Exception>();
private CompositeDisposable Subscriptions = new CompositeDisposable();
public Proxy(IObservable<Maybe<Exception>> errors)
{
Errors = errors;
var subscription = errors.Subscribe(e => currentError = e);
Subscriptions.Add(subscription);
}
public void Dispose()
{
Subscriptions.Dispose();
}
}
protected override object ProvideValueInternal(IServiceProvider serviceProvider, DynamicMarkupExtension.State state )
{
var pvt = serviceProvider as IProvideValueTarget;
if (pvt == null)
{
return null;
}
var frameworkElement = pvt.TargetObject as FrameworkElement;
if (frameworkElement == null)
{
return this;
}
DependencyPropertyChangedEventHandler myd = delegate(object sender, DependencyPropertyChangedEventArgs e){
state.UpdateValue(MakeBinding(serviceProvider, frameworkElement));
};
frameworkElement.DataContextChanged += myd;
return MakeBinding(serviceProvider, frameworkElement);
}
private object MakeBinding(IServiceProvider serviceProvider, FrameworkElement frameworkElement)
{
var dataContext = frameworkElement.DataContext;
if (dataContext is String)
{
return dataContext;
}
ISubject<string> subject = Lens.Empty<string>().Subject;
IObservable<Maybe<Exception>> errors = Observable.Empty<Maybe<Exception>>();
Binding binding;
Proxy proxy = new Proxy(errors);
bool madeit = false;
if (dataContext != null)
{
subject = GetProperty<ISubject<string>>(dataContext, Path);
if (subject != null)
{
errors = GetProperty<IObservable<Maybe<Exception>>>
(dataContext
, ErrorsPath) ?? Observable.Empty<Maybe<Exception>>();
proxy = new Proxy(errors);
}
madeit = true;
}
if(!madeit)
{
subject = new BehaviorSubject<string>("Binding Error");
}
// Bind the subject to the property via a helper ( in private library )
var subscription = subject.TwoWayBindTo(proxy, x => x.Value);
// Make sure we don't leak subscriptions
frameworkElement.Unloaded += (e, v) => subscription.Dispose();
binding = new Binding()
{
Source = proxy,
Path = new System.Windows.PropertyPath("Value"),
ValidatesOnDataErrors = true
};
return binding.ProvideValue(serviceProvider);
}
private static T GetProperty<T>(object context, PropertyPath propPath)
where T : class
{
if (propPath==null)
{
return null;
}
try
{
object propValue = propPath.Path
.Split('.')
.Aggregate(context, (value, name)
=> value.GetType()
.GetProperty(name)
.GetValue(value, null));
return propValue as T;
}
catch (NullReferenceException e)
{
throw new MemberAccessException(propPath.Path + " is not available on " + context.GetType(),e);
}
}
}
}
I want to filter a ObservableCollection with max 3000 items in a DataGrid with 6 columns. The user should be able to filter in an "&&"-way all 6 columns.
Should I use LINQ or a CollectionView for it? LINQ seemed faster trying some www samples. Do you have any pro/cons?
UPDATE:
private ObservableCollection<Material> _materialList;
private ObservableCollection<Material> _materialListInternal;
public MaterialBrowserListViewModel()
{
_materialListInternal = new ObservableCollection<Material>();
for (int i = 0; i < 2222; i++)
{
var mat = new Material()
{
Schoolday = DateTime.Now.Date,
Period = i,
DocumentName = "Excel Sheet" + i,
Keywords = "financial budget report",
SchoolclassCode = "1",
};
_materialListInternal.Add(mat);
var mat1 = new Material()
{
Schoolday = DateTime.Now.Date,
Period = i,
DocumentName = "Word Doc" + i,
Keywords = "Economical staticstics report",
SchoolclassCode = "2",
};
_materialListInternal.Add(mat1);
}
MaterialList = CollectionViewSource.GetDefaultView(MaterialListInternal);
MaterialList.Filter = new Predicate<object>(ContainsInFilter);
}
public bool ContainsInFilter(object item)
{
if (String.IsNullOrEmpty(FilterKeywords))
return true;
Material material = item as Material;
if (DocumentHelper.ContainsCaseInsensitive(material.Keywords,FilterKeywords,StringComparison.CurrentCultureIgnoreCase))
return true;
else
return false;
}
private string _filterKeywords;
public string FilterKeywords
{
get { return _filterKeywords; }
set
{
if (_filterKeywords == value)
return;
_filterKeywords = value;
this.RaisePropertyChanged("FilterKeywords");
MaterialList.Refresh();
}
}
public ICollectionView MaterialList { get; set; }
public ObservableCollection<Material> MaterialListInternal
{
get { return _materialListInternal; }
set
{
_materialListInternal = value;
this.RaisePropertyChanged("MaterialList");
}
}
Using ICollectionView gives you automatic collection changed notifications when you call Refresh. Using LINQ you'll need to fire your own change notifications when the filter needs to be re-run to update the UI. Not difficult, but requires a little more thought than just calling Refresh.
LINQ is more flexible that the simple yes/no filtering used by ICollectionView, but if you're not doing something complex there's not really any advantage to that flexibility.
As Henk stated, there shouldn't be a noticable performance difference in the UI.
For an interactive (DataGrid?) experience you should probabaly use the CollectionView. For a more code-oriented sorting, LINQ.
And with max 3000 items, speed should not be a (major) factor in a UI.
How about both? Thomas Levesque built a LINQ-enabled wrapper around ICollectionView.
Usage:
IEnumerable<Person> people;
// Using query comprehension
var query =
from p in people.ShapeView()
where p.Age >= 18
orderby p.LastName, p.FirstName
group p by p.Country;
query.Apply();
// Using extension methods
people.ShapeView()
.Where(p => p.Age >= 18)
.OrderBy(p => p.LastName)
.ThenBy(p => p.FirstName)
.Apply();
Code:
public static class CollectionViewShaper
{
public static CollectionViewShaper<TSource> ShapeView<TSource>(this IEnumerable<TSource> source)
{
var view = CollectionViewSource.GetDefaultView(source);
return new CollectionViewShaper<TSource>(view);
}
public static CollectionViewShaper<TSource> Shape<TSource>(this ICollectionView view)
{
return new CollectionViewShaper<TSource>(view);
}
}
public class CollectionViewShaper<TSource>
{
private readonly ICollectionView _view;
private Predicate<object> _filter;
private readonly List<SortDescription> _sortDescriptions = new List<SortDescription>();
private readonly List<GroupDescription> _groupDescriptions = new List<GroupDescription>();
public CollectionViewShaper(ICollectionView view)
{
if (view == null)
throw new ArgumentNullException("view");
_view = view;
_filter = view.Filter;
_sortDescriptions = view.SortDescriptions.ToList();
_groupDescriptions = view.GroupDescriptions.ToList();
}
public void Apply()
{
using (_view.DeferRefresh())
{
_view.Filter = _filter;
_view.SortDescriptions.Clear();
foreach (var s in _sortDescriptions)
{
_view.SortDescriptions.Add(s);
}
_view.GroupDescriptions.Clear();
foreach (var g in _groupDescriptions)
{
_view.GroupDescriptions.Add(g);
}
}
}
public CollectionViewShaper<TSource> ClearGrouping()
{
_groupDescriptions.Clear();
return this;
}
public CollectionViewShaper<TSource> ClearSort()
{
_sortDescriptions.Clear();
return this;
}
public CollectionViewShaper<TSource> ClearFilter()
{
_filter = null;
return this;
}
public CollectionViewShaper<TSource> ClearAll()
{
_filter = null;
_sortDescriptions.Clear();
_groupDescriptions.Clear();
return this;
}
public CollectionViewShaper<TSource> Where(Func<TSource, bool> predicate)
{
_filter = o => predicate((TSource)o);
return this;
}
public CollectionViewShaper<TSource> OrderBy<TKey>(Expression<Func<TSource, TKey>> keySelector)
{
return OrderBy(keySelector, true, ListSortDirection.Ascending);
}
public CollectionViewShaper<TSource> OrderByDescending<TKey>(Expression<Func<TSource, TKey>> keySelector)
{
return OrderBy(keySelector, true, ListSortDirection.Descending);
}
public CollectionViewShaper<TSource> ThenBy<TKey>(Expression<Func<TSource, TKey>> keySelector)
{
return OrderBy(keySelector, false, ListSortDirection.Ascending);
}
public CollectionViewShaper<TSource> ThenByDescending<TKey>(Expression<Func<TSource, TKey>> keySelector)
{
return OrderBy(keySelector, false, ListSortDirection.Descending);
}
private CollectionViewShaper<TSource> OrderBy<TKey>(Expression<Func<TSource, TKey>> keySelector, bool clear, ListSortDirection direction)
{
string path = GetPropertyPath(keySelector.Body);
if (clear)
_sortDescriptions.Clear();
_sortDescriptions.Add(new SortDescription(path, direction));
return this;
}
public CollectionViewShaper<TSource> GroupBy<TKey>(Expression<Func<TSource, TKey>> keySelector)
{
string path = GetPropertyPath(keySelector.Body);
_groupDescriptions.Add(new PropertyGroupDescription(path));
return this;
}
private static string GetPropertyPath(Expression expression)
{
var names = new Stack<string>();
var expr = expression;
while (expr != null && !(expr is ParameterExpression) && !(expr is ConstantExpression))
{
var memberExpr = expr as MemberExpression;
if (memberExpr == null)
throw new ArgumentException("The selector body must contain only property or field access expressions");
names.Push(memberExpr.Member.Name);
expr = memberExpr.Expression;
}
return String.Join(".", names.ToArray());
}
}
Credit:
http://www.thomaslevesque.com/2011/11/30/wpf-using-linq-to-shape-data-in-a-collectionview/
Based on a visual complexity and number of items there really WILL be a noticable performance difference since the Refresh method recreates the whole view!!!
You need my ObservableComputations library. Using this library you can code like this:
ObservableCollection<Material> MaterialList = MaterialListInternal.Filtering(m =>
String.IsNullOrEmpty(FilterKeywords)
|| DocumentHelper.ContainsCaseInsensitive(
material.Keywords, FilterKeywords, StringComparison.CurrentCultureIgnoreCase));
MaterialList reflects all the changes in the MaterialListInternal collection. Do not forget to add the implementation of the INotifyPropertyChanged interface to Material class, so that MaterialList collection reflects the changes in material.Keywords property.