sources/Google.Solutions.Mvvm/Controls/BindableListView.cs (250 lines of code) (raw):

// // Copyright 2020 Google LLC // // Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. // using Google.Solutions.Common.Linq; using System; using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; using System.Diagnostics; using System.Drawing; using System.Linq; using System.Windows.Forms; namespace Google.Solutions.Mvvm.Controls { /// <summary> /// Listview that support simple data binding. /// </summary> public class BindableListView<TModelItem> : ListView { private ICollection<TModelItem>? model; private readonly Dictionary<int, Func<TModelItem, string>> columnAccessors = new Dictionary<int, Func<TModelItem, string>>(); private Func<TModelItem, int>? imageIndexAccessor = null; [Browsable(true)] public bool AutoResizeColumnsOnUpdate { get; set; } = false; private string? ExtractColumnValue(int columnIndex, TModelItem modelItem) { if (this.columnAccessors.TryGetValue(columnIndex, out var accessorFunc)) { return accessorFunc(modelItem); } else { return null; } } private int ExtractImageIndex(TModelItem modelItem) { return this.imageIndexAccessor == null ? 0 : this.imageIndexAccessor(modelItem); } private ListViewItem FindViewItem(TModelItem modelItem) { return this.Items .OfType<ListViewItem>() .FirstOrDefault(item => Equals(item.Tag, modelItem)); } private void AddViewItems(IEnumerable<TModelItem> items) { foreach (var item in items) { ObserveItem(item); } this.Items.AddRange(items .Select(item => new ListViewItem( this.Columns .OfType<ColumnHeader>() .Select(c => ExtractColumnValue(c.Index, item)) .ToArray()) { Tag = item, ImageIndex = ExtractImageIndex(item) }).ToArray()); } //--------------------------------------------------------------------- // Ctor. //--------------------------------------------------------------------- public BindableListView() { // // By default, double buffering is off. Especially in dark mode, // this causes visible flickering. // this.DoubleBuffered = true; } //--------------------------------------------------------------------- // Overrides. //--------------------------------------------------------------------- protected override void ScaleControl(SizeF factor, BoundsSpecified specified) { base.ScaleControl(factor, specified); // // Rescale existing columns. Columns that are added // later need to be rescaled explicitly. // foreach (ColumnHeader column in this.Columns) { column.Width = LogicalToDeviceUnits(column.Width); } } //--------------------------------------------------------------------- // Selection properties. //--------------------------------------------------------------------- public event EventHandler? SelectedModelItemsChanged; public event EventHandler? SelectedModelItemChanged; protected override void OnSelectedIndexChanged(EventArgs e) { this.SelectedModelItemsChanged?.Invoke(this, e); this.SelectedModelItemChanged?.Invoke(this, e); } public IEnumerable<TModelItem> SelectedModelItems { get => this.SelectedItems .OfType<ListViewItem>() .Select(item => (TModelItem)item.Tag); set { this.SelectedIndices.Clear(); if (value != null) { foreach (var selectedItem in value) { var index = this.Items.IndexOf(FindViewItem(selectedItem)); Debug.Assert(index >= 0); this.SelectedIndices.Add(index); } } this.SelectedModelItemsChanged?.Invoke(this, EventArgs.Empty); } } public TModelItem SelectedModelItem { get => this.SelectedItems .OfType<ListViewItem>() .Select(item => (TModelItem)item.Tag) .FirstOrDefault(); set { this.SelectedIndices.Clear(); if (value != null) { var index = this.Items.IndexOf(FindViewItem(value)); Debug.Assert(index >= 0); this.SelectedIndices.Add(index); } this.SelectedModelItemChanged?.Invoke(this, EventArgs.Empty); } } //--------------------------------------------------------------------- // List Binding. //--------------------------------------------------------------------- public void BindCollection(ICollection<TModelItem> model) { // Reset. if (this.model != null) { UnobserveItems(this.model); if (this.model is INotifyCollectionChanged observable) { observable.CollectionChanged -= Model_CollectionChanged; } } this.Items.Clear(); // Configure control. this.model = model; if (this.model != null) { AddViewItems(this.model); if (this.model is INotifyCollectionChanged observable) { observable.CollectionChanged += Model_CollectionChanged; } } } public void BindColumn(int columnIndex, Func<TModelItem, string> accessorFunc) { this.columnAccessors[columnIndex] = accessorFunc; } public void BindImageIndex(Func<TModelItem, int> accessorFunc) { this.imageIndexAccessor = accessorFunc; } //--------------------------------------------------------------------- // Change event handlers. //--------------------------------------------------------------------- private void ObserveItem(TModelItem item) { if (item is INotifyPropertyChanged observableItem) { observableItem.PropertyChanged += ModelItem_PropertyChanged; } } private void UnobserveItems(IEnumerable<TModelItem> items) { foreach (var item in items) { UnobserveItem(item); } } private void UnobserveItem(TModelItem item) { if (item is INotifyPropertyChanged observableItem) { observableItem.PropertyChanged -= ModelItem_PropertyChanged; } } private void Model_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { switch (e.Action) { case NotifyCollectionChangedAction.Add: AddViewItems(e.NewItems.OfType<TModelItem>()); break; case NotifyCollectionChangedAction.Remove: foreach (var oldModelItem in e.OldItems.OfType<TModelItem>()) { var oldViewItem = FindViewItem(oldModelItem); if (oldViewItem != null) { UnobserveItem(oldModelItem); this.Items.Remove(oldViewItem); } } break; case NotifyCollectionChangedAction.Replace: if (e.OldItems.Count == e.NewItems.Count) { var count = e.OldItems.Count; for (var i = 0; i < count; i++) { var oldModelItem = (TModelItem)e.OldItems[i]; var newModelItem = (TModelItem)e.NewItems[i]; var viewItem = FindViewItem(oldModelItem); if (viewItem != null) { UnobserveItem(oldModelItem); ObserveItem(newModelItem); viewItem.Tag = newModelItem; foreach (ColumnHeader column in this.Columns) { viewItem.SubItems[column.Index].Text = ExtractColumnValue(column.Index, newModelItem); } } } } break; case NotifyCollectionChangedAction.Move: foreach (var oldItem in e.OldItems.OfType<TModelItem>()) { var viewItem = FindViewItem(oldItem); if (oldItem != null) { this.Items.Remove(viewItem); this.Items.Insert(e.NewStartingIndex, viewItem); } } break; case NotifyCollectionChangedAction.Reset: // Reload everything. UnobserveItems(this.Items.Cast<ListViewItem>() .Select(i => i.Tag) .OfType<TModelItem>()); this.Items.Clear(); AddViewItems(this.model.EnsureNotNull()); break; default: break; } if (this.AutoResizeColumnsOnUpdate) { AutoResizeColumns(ColumnHeaderAutoResizeStyle.ColumnContent); AutoResizeColumns(ColumnHeaderAutoResizeStyle.HeaderSize); } } private void ModelItem_PropertyChanged(object sender, PropertyChangedEventArgs e) { // Rehydrate the entire list item. var modelItem = (TModelItem)sender; var viewItem = FindViewItem(modelItem); if (viewItem != null) { viewItem.ImageIndex = ExtractImageIndex(modelItem); foreach (var column in this.Columns.OfType<ColumnHeader>()) { viewItem.SubItems[column.Index].Text = ExtractColumnValue(column.Index, modelItem); } } } } }