Virtualizing DataGrid over ListCollectionView - wpf

I am using a ListCollectionView over a custom list which provides read access to a particular database table. Below is the definition of the custom list.
class MaterialCollection : IList
{
#region Fields
private Object syncRoot;
private SQLiteConnection connection;
#endregion
#region IList Interface
public object this[int index]
{
get
{
using (SQLiteCommand command = connection.CreateCommand())
{
command.CommandText = "SELECT * FROM Materials LIMIT 1 OFFSET #Index";
command.Parameters.AddWithValue("#Index", index);
using (SQLiteDataReader reader = command.ExecuteReader())
{
if (reader.Read())
{
return GetMaterial(reader);
}
else
{
throw new Exception();
}
}
}
}
set
{
throw new NotImplementedException();
}
}
public int Count
{
get
{
using (SQLiteCommand command = connection.CreateCommand())
{
command.CommandText = "SELECT Count(*) FROM Materials";
return Convert.ToInt32(command.ExecuteScalar());
}
}
}
public bool IsFixedSize
{
get
{
return false;
}
}
public bool IsReadOnly
{
get
{
return true;
}
}
public bool IsSynchronized
{
get
{
return true;
}
}
public object SyncRoot
{
get
{
return this.syncRoot;
}
}
public IEnumerator GetEnumerator()
{
return Enumerate().GetEnumerator();
}
#endregion
#region Constructor
public MaterialCollection(SQLiteConnection connection)
{
this.connection = connection;
this.syncRoot = new Object();
}
#endregion
#region Private Methods
private Material GetMaterial(SQLiteDataReader reader)
{
int id = Convert.ToInt32(reader["Id"]);
string materialNumber = Convert.ToString(reader["MaterialNumber"]);
string type = Convert.ToString(reader["Type"]);
string description = Convert.ToString(reader["Description"]);
string alternateDescription = Convert.ToString(reader["AlternateDescription"]);
string tags = Convert.ToString(reader["Tags"]);
Material material = new Material();
material.Id = id;
material.MaterialNumber = materialNumber;
material.Type = (MaterialType)Enum.Parse(typeof(MaterialType), type);
material.Description = description;
material.AlternateDescription = alternateDescription;
material.Tags = tags;
return material;
}
private IEnumerable<Material> Enumerate()
{
using (SQLiteCommand command = connection.CreateCommand())
{
command.CommandText = "SELECT * FROM Materials";
using (SQLiteDataReader reader = command.ExecuteReader())
{
while (reader.Read())
{
yield return GetMaterial(reader);
}
}
}
}
#endregion
#region Unimplemented Functions
public int Add(object value)
{
throw new NotImplementedException();
}
public void Clear()
{
throw new NotImplementedException();
}
public bool Contains(object value)
{
throw new NotImplementedException();
}
public void CopyTo(Array array, int index)
{
throw new NotImplementedException();
}
public int IndexOf(object value)
{
return 0;
}
public void Insert(int index, object value)
{
throw new NotImplementedException();
}
public void Remove(object value)
{
throw new NotImplementedException();
}
public void RemoveAt(int index)
{
throw new NotImplementedException();
}
#endregion
}
The problem is that during the initial population of the DataGrid, the this[int index] method is called for every single row (i.e. 0 through Count - 1).
As the table contains potentially millions of rows and read operations are costly (i.e. SQLite on a legacy HDD), such an operation is unacceptable. Furthermore, it doesn't make sense to load all of the millions of rows when the screen can only accommodate for 50 or so items.
I would like to make it such that the DataGrid fetches only the row that it is displaying, instead of fetching the entire table. How is such a working possible?
(P.S. I have ensured that virtualizatin is enabled for the DataGrid itself)

It seems that the problem was resolved after adding ScrollViewer.CanContentScroll="True" to the ListView.

Related

WPF: store objects inside application settings.settings file

I build a simple ClipboardManager that hold all last Copy item.
So i have this simple ClipboardItem class:
public class ClipboardItem : INotifyPropertyChanged
{
private string _text { get; set; }
private int _index { get; set; }
public string Text
{
get { return _text; }
set
{
_text = value;
NotifyPropertyChanged();
}
}
public int Index
{
get { return _index; }
set
{
_index = value;
NotifyPropertyChanged();
}
}
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
}
And my ViewModel class that hold ObservableCollection<ClipboardItem>:
public class ViewModel : INotifyPropertyChanged
{
private ObservableCollection<ClipboardItem> _clipboards;
public ViewModel()
{
if (_clipboards == null)
{
_clipboards = new ObservableCollection<ClipboardItem>();
_clipboards.CollectionChanged += _clipboards_CollectionChanged;
}
}
private void _clipboards_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
for (int i = 0; i < _clipboards.Count; i++)
_clipboards[i].Index = i + 1;
}
public ObservableCollection<ClipboardItem> Clipboards
{
get { return _clipboards; }
set
{
_clipboards = value;
NotifyPropertyChanged();
}
}
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
}
So every Copy create new ClipboardItem object inside list but when i restart the application all the records gone so i wonder if there any way to store all my ClipboardItem object inside the application settings.settings file.
//Variable Creation
private string _dataFileName = #"ClipData.xml";
DataTable _clipDataTable = new DataTable();
Inside ViewModel constructor.
public ViewModel()
{
if (_clipboards == null)
{
_clipboards = new ObservableCollection<ClipboardItem>();
_clipboards.CollectionChanged += _clipboards_CollectionChanged;
}
InitDataTable();
ReadDataFile();
}
Create new Methods
/// <summary>
/// Initialize Data Table considering you have only 1 column data.
/// If you have more then you need to create more columns
/// </summary>
private void InitDataTable()
{
_clipDataTable = new DataTable();
_clipDataTable.Columns.Add("ClipHeader");
_clipDataTable.AcceptChanges();
}
//the clipboard Data is saved in xml file.
private void WriteDataFile()
{
DataSet ClipDataSet = new DataSet();
ClipDataSet.Tables.Add(_clipDataTable);
ClipDataSet.WriteXml(_dataFileName);
}
// if file exits then read the xml file and add it to the Collection, which will be reflected in UI.
private void ReadDataFile()
{
DataSet ClipDataSet = new DataSet();
if (File.Exists(_dataFileName))
{
ClipDataSet.ReadXml(_dataFileName);
foreach (DataRow item in ClipDataSet.Tables[0].Rows)
{
Clipboards.Add(new ClipboardItem { Text = Convert.ToString(item["ClipHeader"]) });
}
}
}
Using Command you can bind Method of ViewModel to Window Closing event. So whenever the user closes the window, the data in the collection will be written into the Xml file.
Data from the Collection is copied into DataTable.
private void WindowCloseCommadn(object o)
{
foreach (var item in Clipboards)
{
DataRow dataRow = _clipDataTable.NewRow();
dataRow["ClipHeader"] = item.Text;
_clipDataTable.Rows.Add(dataRow);
}
WriteDataFile();
}
Update:-
Same Code without ViewModel, by making the codebehind class has the DataContext for binding Collection.
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private ClipboardMonitor clipboardMonitor;
private string _dataFileName = #"ClipData.xml";
DataTable _clipDataTable = new DataTable();
public ObservableCollection<ClipboardItem> Clipboards { get; set; }
public MainWindow()
{
InitializeComponent();
Clipboards = new ObservableCollection<ClipboardItem>();
Clipboards.CollectionChanged += Clipboards_CollectionChanged;
Loaded += MainWindow_Loaded;
InitiateClipboardMonitor();
this.Closing += MainWindow_Closing1;
this.DataContext = this;
}
private void MainWindow_Closing1(object sender, CancelEventArgs e)
{
foreach (var item in Clipboards)
{
DataRow dataRow = _clipDataTable.NewRow();
dataRow["ClipHeader"] = item.Text;
_clipDataTable.Rows.Add(dataRow);
}
WriteDataFile();
}
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
InitDataTable();
ReadDataFile();
}
/// <summary>
/// Initialize Data Table considering you have only 1 column data.
/// If you have more then you need to create more columns
/// </summary>
private void InitDataTable()
{
_clipDataTable = new DataTable();
_clipDataTable.Columns.Add("ClipHeader");
_clipDataTable.AcceptChanges();
}
private void WriteDataFile()
{
DataSet ClipDataSet = new DataSet();
ClipDataSet.Tables.Add(_clipDataTable);
ClipDataSet.WriteXml(_dataFileName);
}
private void ReadDataFile()
{
DataSet ClipDataSet = new DataSet();
if (File.Exists(_dataFileName))
{
ClipDataSet.ReadXml(_dataFileName);
foreach (DataRow item in ClipDataSet.Tables[0].Rows)
{
Clipboards.Add(new ClipboardItem { Text = Convert.ToString(item["ClipHeader"]) });
}
}
}
private void Clipboards_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
for (int i = 0; i < Clipboards.Count; i++)
{
Clipboards[i].Index = i + 1;
}
}
private void InitiateClipboardMonitor()
{
clipboardMonitor = new ClipboardMonitor();
clipboardMonitor.OnClipboardContentChanged += ClipboardMonitor_OnClipboardContentChanged; ;
}
private void ClipboardMonitor_OnClipboardContentChanged(object sender, EventArgs e)
{
string clipboardText = Clipboard.GetText(TextDataFormat.Text);
Clipboards.Add(new ClipboardItem { Text = clipboardText });
}
}
to know about Command, refer the article
https://www.codeproject.com/Articles/274982/Commands-in-MVVM

Validation on WPF Options

I have been sitting on the internet now for 3hours with not much help.
I am trying to implement validation on my UI with the following requirements using the MVVM principle.
Currently by using DataAnnotations on my model:
Example:
private string _name;
[Required(ErrorMessage = "Name must be filled")]
public string Name
{
get { return _name; }
set { this.Update(x => x.Name, () => _name= value, _name, value); }
}
1) I want the validation only to be done when I click on a button (Submit)
2) If I have lets say 5 validations on the UI I want to display them in a list also.
I had a look at several ways and not sure which to use for best practices that suits my 2 requirements the best:
IDataInfoError?
INotifyDataErrorInfo?
DataAnnotations? (Current implementation)
Anybody that can point me in the right direction, tips anything?
You should read the following blog post: https://blog.magnusmontin.net/2013/08/26/data-validation-in-wpf/. It provides an example of you could implement validation using data annotations and the INotifyDataErrorInfo interface:
public class ViewModel : INotifyDataErrorInfo
{
private readonly Dictionary<string, ICollection<string>>
_validationErrors = new Dictionary<string, ICollection<string>>();
private readonly Model _user = new Model();
public string Username
{
get { return _user.Username; }
set
{
_user.Username = value;
ValidateModelProperty(value, "Username");
}
}
public string Name
{
get { return _user.Name; }
set
{
_user.Name = value;
ValidateModelProperty(value, "Name");
}
}
protected void ValidateModelProperty(object value, string propertyName)
{
if (_validationErrors.ContainsKey(propertyName))
_validationErrors.Remove(propertyName);
ICollection<ValidationResult> validationResults = new List<ValidationResult>();
ValidationContext validationContext =
new ValidationContext(_user, null, null) { MemberName = propertyName };
if (!Validator.TryValidateProperty(value, validationContext, validationResults))
{
_validationErrors.Add(propertyName, new List<string>());
foreach (ValidationResult validationResult in validationResults)
{
_validationErrors[propertyName].Add(validationResult.ErrorMessage);
}
}
RaiseErrorsChanged(propertyName);
}
protected void ValidateModel()
{
_validationErrors.Clear();
ICollection<ValidationResult> validationResults = new List<ValidationResult>();
ValidationContext validationContext = new ValidationContext(_user, null, null);
if (!Validator.TryValidateObject(_user, validationContext, validationResults, true))
{
foreach (ValidationResult validationResult in validationResults)
{
string property = validationResult.MemberNames.ElementAt(0);
if (_validationErrors.ContainsKey(property))
{
_validationErrors[property].Add(validationResult.ErrorMessage);
}
else
{
_validationErrors.Add(property, new List<string> { validationResult.ErrorMessage });
}
}
}
/* Raise the ErrorsChanged for all properties explicitly */
RaiseErrorsChanged("Username");
RaiseErrorsChanged("Name");
}
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
private void RaiseErrorsChanged(string propertyName)
{
if (ErrorsChanged != null)
ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
}
public System.Collections.IEnumerable GetErrors(string propertyName)
{
if (string.IsNullOrEmpty(propertyName)
|| !_validationErrors.ContainsKey(propertyName))
return null;
return _validationErrors[propertyName];
}
public bool HasErrors
{
get { return _validationErrors.Count > 0; }
}
}

WPF Binding in columns not working

I have a list ob object like this:
public class Device : ObjectBase
{
private int _DeviceNbr;
public int DeviceNbr
{
get { return _DeviceNbr; }
set { _DeviceNbr = value; }
}
private string _DeviceName;
public string DeviceName
{
get { return _DeviceName; }
set { _DeviceName = value; OnPropertyChanged(); }
}
private ObservableCollection<State> _DeviceStates;
public ObservableCollection<State> DeviceStates
{
get { return _DeviceStates; }
set { _DeviceStates = value; OnPropertyChanged(); }
}
}
public class State: ObjectBase
{
public int StateNbr { get; set; }
private string _stateType;
public string StateType
{
get { return _stateType; }
set { _stateType = value; }
}
private int _value;
public int Value
{
get { return _value; }
set { _value = value; OnPropertyChanged(); }
}
}
which I need to bind to a Datagrid.
My approach is to create a customDataGrid which looks like this:
public class CustomGrid : DataGrid
{
public ObservableCollection<ColumnConfig> ColumnConfigs
{
get { return GetValue(ColumnConfigsProperty) as ObservableCollection<ColumnConfig>; }
set { SetValue(ColumnConfigsProperty, value); }
}
public static readonly DependencyProperty ColumnConfigsProperty =
DependencyProperty.Register("ColumnConfigs", typeof(ObservableCollection<ColumnConfig>), typeof(CustomGrid), new PropertyMetadata(new PropertyChangedCallback(OnColumnsChanged)));
static void OnColumnsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var dataGrid = d as CustomGrid;
dataGrid.Columns.Clear();
dataGrid.Columns.Add(new DataGridTextColumn() { Header = "Nbr", Binding = new Binding("DeviceNbr") });
dataGrid.Columns.Add(new DataGridTextColumn() { Header = "Device", Binding = new Binding("DeviceName") });
foreach (var columnConfig in dataGrid.ColumnConfigs.Where(c => c.IsVisible))
{
var column = new DataGridTextColumn()
{
Header = columnConfig.ColumnHeader,
Binding = new Binding("DeviceStates")
{
ConverterParameter = columnConfig.ColumnName,
Converter = new DeviceStateConverter(),
UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged,
Mode =BindingMode.TwoWay
}
};
dataGrid.Columns.Add(column);
}
}
}
public class DeviceStateConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is ObservableCollection<State> DeviceStates && parameter != null)
{
var DeviceState = DeviceStates.FirstOrDefault(s => s.StateType == parameter.ToString());
if (DeviceState != null)
return DeviceState.Value;
}
return false;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
The ViewModel looks like this:
public class MainViewModel : ObjectBase
{
private ObservableCollection<Device> _Devices;
public ObservableCollection<Device> Devices
{
get { return _Devices; }
set { _Devices = value; OnPropertyChanged(); }
}
private ObservableCollection<ColumnConfig> _columnConfigs;
public ObservableCollection<ColumnConfig> ColumnConfigs
{
get { return _columnConfigs; }
set { _columnConfigs = value; OnPropertyChanged(); }
}
public MainViewModel()
{
Devices = new ObservableCollection<Device>();
_columnConfigs = new ObservableCollection<ColumnConfig>()
{
new ColumnConfig(){ ColumnHeader = "On", ColumnName = "On", ColumnWidth= 100, IsVisible= true},
new ColumnConfig(){ ColumnHeader = "Off", ColumnName = "Off", ColumnWidth= 100, IsVisible= true}
};
for ( int i = 0; i <= 100; i++)
{
_Devices.Add(new Device()
{
DeviceNbr = i,
DeviceName = "Device " + i.ToString(),
DeviceStates = new ObservableCollection<State>()
{
new State() { StateType = "On", Value= i},
new State() { StateType = "Off", Value= i+1}
}
});
}
OnPropertyChanged("ColumnConfigs");
OnPropertyChanged("Devices");
}
public void TestStateChange ()
{
Devices[2].DeviceName = "Device X";
Devices[2].DeviceStates[0].Value = 5;
// OnPropertyChanged("Devices");
}
}
And the XAML like this:
<local:CustomGrid
AutoGenerateColumns="False"
ColumnConfigs="{Binding ColumnConfigs}"
ItemsSource="{Binding Devices, UpdateSourceTrigger=PropertyChanged, Mode=OneWay}" />
Here is the result:
Application
Now the problem is that the binding for the Devicesstates does not work.
In the ViewModel is a method called "TestStateChange" where tried to change that state. The state in the DeviceStatesCollection changed like expected but it doesn't reflect to the view. Can someone please provide me some help?
UPDATE
The binding works but PropertyChanged Event when changing the value of a state does not fire.
public void TestStateChange()
{
foreach (var device in Devices)
{
foreach (var state in device.DeviceStates)
{
state.Value = state.Value + 1;
}
device.OnPropertyChanged("DeviceStates");
}
}
So I have to raise the PropertyChangedEvent on the "parent" collection of the state. That's weird.
The only think that I can now think of, is to implement an event in the State class and let the parent collection object subscribe to it.
Does someone has a better idea?
If your State object implements INotifyPropertyChanged (which all your bindable data objects should), and if your datagrid column binds directly to a State object (i.e. via a binding path that contains an index) then your UI will update as you expect.
You could subscribe to the State property change events from the parent Device object and then re-notify from there, but that is a long winded way to do it and means that you have to subscribe to the CollectionChanged event of the ObservableCollection that contains the State objects so that you can attach/unattach from the PropertyChange event on each State object (sometimes this will be the only way, but avoid it if you can).

Databind a read only dependency property to ViewModel in Xaml

I'm trying to databind a Button's IsMouseOver read-only dependency property to a boolean read/write property in my view model.
Basically I need the Button's IsMouseOver property value to be read to a view model's property.
<Button IsMouseOver="{Binding Path=IsMouseOverProperty, Mode=OneWayToSource}" />
I'm getting a compile error: 'IsMouseOver' property is read-only and cannot be set from markup. What am I doing wrong?
No mistake. This is a limitation of WPF - a read-only property cannot be bound OneWayToSource unless the source is also a DependencyProperty.
An alternative is an attached behavior.
As many people have mentioned, this is a bug in WPF and the best way is to do it is attached property like Tim/Kent suggested. Here is the attached property I use in my project. I intentionally do it this way for readability, unit testability, and sticking to MVVM without codebehind on the view to handle the events manually everywhere.
public interface IMouseOverListener
{
void SetIsMouseOver(bool value);
}
public static class ControlExtensions
{
public static readonly DependencyProperty MouseOverListenerProperty =
DependencyProperty.RegisterAttached("MouseOverListener", typeof (IMouseOverListener), typeof (ControlExtensions), new PropertyMetadata(OnMouseOverListenerChanged));
private static void OnMouseOverListenerChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var element = ((UIElement)d);
if(e.OldValue != null)
{
element.MouseEnter -= ElementOnMouseEnter;
element.MouseLeave -= ElementOnMouseLeave;
}
if(e.NewValue != null)
{
element.MouseEnter += ElementOnMouseEnter;
element.MouseLeave += ElementOnMouseLeave;
}
}
public static void SetMouseOverListener(UIElement element, IMouseOverListener value)
{
element.SetValue(MouseOverListenerProperty, value);
}
public static IMouseOverListener GetMouseOverListener(UIElement element)
{
return (IMouseOverListener) element.GetValue(MouseOverListenerProperty);
}
private static void ElementOnMouseLeave(object sender, MouseEventArgs mouseEventArgs)
{
var element = ((UIElement)sender);
var listener = GetMouseOverListener(element);
if(listener != null)
listener.SetIsMouseOver(false);
}
private static void ElementOnMouseEnter(object sender, MouseEventArgs mouseEventArgs)
{
var element = ((UIElement)sender);
var listener = GetMouseOverListener(element);
if (listener != null)
listener.SetIsMouseOver(true);
}
}
Here's a rough draft of what i resorted to while seeking a general solution to this problem. It employs a css-style formatting to specify Dependency-Properties to be bound to model properties (models gotten from the DataContext); this also means it will work only on FrameworkElements.
I haven't thoroughly tested it, but the happy path works just fine for the few test cases i ran.
public class BindingInfo
{
internal string sourceString = null;
public DependencyProperty source { get; internal set; }
public string targetProperty { get; private set; }
public bool isResolved => source != null;
public BindingInfo(string source, string target)
{
this.sourceString = source;
this.targetProperty = target;
validate();
}
private void validate()
{
//verify that targetProperty is a valid c# property access path
if (!targetProperty.Split('.')
.All(p => Identifier.IsMatch(p)))
throw new Exception("Invalid target property - " + targetProperty);
//verify that sourceString is a [Class].[DependencyProperty] formatted string.
if (!sourceString.Split('.')
.All(p => Identifier.IsMatch(p)))
throw new Exception("Invalid source property - " + sourceString);
}
private static readonly Regex Identifier = new Regex(#"[_a-z][_\w]*$", RegexOptions.IgnoreCase);
}
[TypeConverter(typeof(BindingInfoConverter))]
public class BindingInfoGroup
{
private List<BindingInfo> _infoList = new List<BindingInfo>();
public IEnumerable<BindingInfo> InfoList
{
get { return _infoList.ToArray(); }
set
{
_infoList.Clear();
if (value != null) _infoList.AddRange(value);
}
}
}
public class BindingInfoConverter: TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
if (sourceType == typeof(string)) return true;
return base.CanConvertFrom(context, sourceType);
}
// Override CanConvertTo to return true for Complex-to-String conversions.
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
if (destinationType == typeof(string)) return true;
return base.CanConvertTo(context, destinationType);
}
// Override ConvertFrom to convert from a string to an instance of Complex.
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
string text = value as string;
return new BindingInfoGroup
{
InfoList = text.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)
.Select(binfo =>
{
var parts = binfo.Split(new[] { ':' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length != 2) throw new Exception("invalid binding info - " + binfo);
return new BindingInfo(parts[0].Trim(), parts[1].Trim());
})
};
}
// Override ConvertTo to convert from an instance of Complex to string.
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture,
object value, Type destinationType)
{
var bgroup = value as BindingInfoGroup;
return bgroup.InfoList
.Select(bi => $"{bi.sourceString}:{bi.targetProperty};")
.Aggregate((n, p) => n += $"{p} ")
.Trim();
}
public override bool GetStandardValuesSupported(ITypeDescriptorContext context) => false;
}
public class Bindings
{
#region Fields
private static ConcurrentDictionary<DependencyProperty, PropertyChangeHandler> _Properties =
new ConcurrentDictionary<DependencyProperty, PropertyChangeHandler>();
#endregion
#region OnewayBindings
public static readonly DependencyProperty OnewayBindingsProperty =
DependencyProperty.RegisterAttached("OnewayBindings", typeof(BindingInfoGroup), typeof(Bindings), new FrameworkPropertyMetadata
{
DefaultValue = null,
PropertyChangedCallback = (x, y) =>
{
var fwe = x as FrameworkElement;
if (fwe == null) return;
//resolve the bindings
resolve(fwe);
//add change delegates
(GetOnewayBindings(fwe)?.InfoList ?? new BindingInfo[0])
.Where(bi => bi.isResolved)
.ToList()
.ForEach(bi =>
{
var descriptor = DependencyPropertyDescriptor.FromProperty(bi.source, fwe.GetType());
PropertyChangeHandler listener = null;
if (_Properties.TryGetValue(bi.source, out listener))
{
descriptor.RemoveValueChanged(fwe, listener.callback); //cus there's no way to check if it had one before...
descriptor.AddValueChanged(fwe, listener.callback);
}
});
}
});
private static void resolve(FrameworkElement element)
{
var bgroup = GetOnewayBindings(element);
bgroup.InfoList
.ToList()
.ForEach(bg =>
{
//source
var sourceParts = bg.sourceString.Split('.');
if (sourceParts.Length == 1)
{
bg.source = element.GetType()
.baseTypes() //<- flattens base types, including current type
.SelectMany(t => t.GetRuntimeFields()
.Where(p => p.IsStatic)
.Where(p => p.FieldType == typeof(DependencyProperty)))
.Select(fi => fi.GetValue(null) as DependencyProperty)
.FirstOrDefault(dp => dp.Name == sourceParts[0])
.ThrowIfNull($"Dependency Property '{sourceParts[0]}' was not found");
}
else
{
//resolve the dependency property [ClassName].[PropertyName]Property - e.g FrameworkElement.DataContextProperty
bg.source = Type.GetType(sourceParts[0])
.GetField(sourceParts[1])
.GetValue(null)
.ThrowIfNull($"Dependency Property '{bg.sourceString}' was not found") as DependencyProperty;
}
_Properties.GetOrAdd(bg.source, ddp => new PropertyChangeHandler { property = ddp }); //incase it wasnt added before.
});
}
public static BindingInfoGroup GetOnewayBindings(FrameworkElement source)
=> source.GetValue(OnewayBindingsProperty) as BindingInfoGroup;
public static void SetOnewayBindings(FrameworkElement source, string value)
=> source.SetValue(OnewayBindingsProperty, value);
#endregion
}
public class PropertyChangeHandler
{
internal DependencyProperty property { get; set; }
public void callback(object obj, EventArgs args)
{
var fwe = obj as FrameworkElement;
var target = fwe.DataContext;
if (fwe == null) return;
if (target == null) return;
var bg = Bindings.GetOnewayBindings(fwe);
if (bg == null) return;
else bg.InfoList
.Where(bi => bi.isResolved)
.Where(bi => bi.source == property)
.ToList()
.ForEach(bi =>
{
//transfer data to the object
var data = fwe.GetValue(property);
KeyValuePair<object, PropertyInfo>? pinfo = resolveProperty(target, bi.targetProperty);
if (pinfo == null) return;
else pinfo.Value.Value.SetValue(pinfo.Value.Key, data);
});
}
private KeyValuePair<object, PropertyInfo>? resolveProperty(object target, string path)
{
try
{
var parts = path.Split('.');
if (parts.Length == 1) return new KeyValuePair<object, PropertyInfo>(target, target.GetType().GetProperty(parts[0]));
else //(parts.Length>1)
return resolveProperty(target.GetType().GetProperty(parts[0]).GetValue(target),
string.Join(".", parts.Skip(1)));
}
catch (Exception e) //too lazy to care :D
{
return null;
}
}
}
And to use the XAML...
<Grid ab:Bindings.OnewayBindings="IsMouseOver:mouseOver;">...</Grid>

Filter a collection with LINQ vs CollectionView

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.

Resources