using System; using System.Collections.Generic; using System.Data; using System.Linq; using System.Threading; using System.Threading.Tasks; using Vanara.Extensions; using Vanara.PInvoke; using static Vanara.PInvoke.Ole32; using static Vanara.PInvoke.Shell32; namespace Vanara.Windows.Shell { /// Represents a that is populated asynchronously with information about shell items. /// public class ShellDataTable : DataTable { private const string colId = "IDList"; private const string extAlign = "ColAlign"; private const string extPropKey = "PropertyKey"; private const string extSlow = "Slow"; private const string extState = "ColState"; private readonly FolderItemFilter itemFilter; private readonly ShellFolder parent; private DataColumn[] colsToGet; private List defCols; private IEnumerable items; /// Initializes a new instance of the class with a list of shell items. /// The items for which to collect information. public ShellDataTable(IEnumerable items) : base() { BuildColumns(ShellFolder.Desktop); this.items = items.Select(i => i.PIDL).ToArray(); } /// Initializes a new instance of the class with the items from a shell folder. /// The folder whose items are to be retrieved. /// The filter to determine which child items of the folder are enumerated. public ShellDataTable(ShellFolder folder, FolderItemFilter filter = FolderItemFilter.Folders | FolderItemFilter.NonFolders) : base(folder.ParsingName) { itemFilter = filter; BuildColumns(parent = folder); } /// Occurs when all rows have been added in a call to populate the table with their fast properties. public event EventHandler AllFastRowsAdded; /// Occurs when all rows have been added in a call to populate the table with all (fast and slow) properties. public event EventHandler TableLoaded; /// Gets the columns that should be on by default in Details view. /// The default columns. public IReadOnlyList DefaultColumns => (IReadOnlyList)defCols; /// Gets a column's visual alignment. /// The column to check. /// The suggested alignment of the column when displayed. public ComCtl32.ListViewColumnFormat GetColumnAlignment(DataColumn column) => (ComCtl32.ListViewColumnFormat)column.ExtendedProperties[extAlign]; /// Gets the column property key. /// The column to check. /// The property key associated with the column. public PROPERTYKEY GetColumnPropertyKey(DataColumn column) => (PROPERTYKEY)column.ExtendedProperties[extPropKey]; /// Gets the state of the column. /// The column to check. /// A value describing how the column values should be treated. public SHCOLSTATE GetColumnState(DataColumn column) => (SHCOLSTATE)column.ExtendedProperties[extState]; /// Gets the PIDL for the row. /// The row to check. /// The PIDL from the row. public PIDL GetPIDL(DataRow row) => row[colId] is null || row[colId] == DBNull.Value ? PIDL.Null : new PIDL((byte[])row[colId]); /// Determines whether the specified column takes longer to retrieve. /// The column to check. /// if the column takes longer to retrieve; otherwise, . public bool IsColumnSlow(DataColumn column) => (bool)column.ExtendedProperties[extSlow]; /// Populates the table with all the requested shell items. /// The names of the columns to populate. /// The cancellation token. public async Task PopulateTableAsync(IEnumerable columns, CancellationToken cancellationToken) { var columnsToGet = columns.Where(n => n != colId).Select(n => Columns[n]).ToArray(); await PopulateTableAsync(columnsToGet, cancellationToken); } /// Populates the table with all the requested shell items. /// The PROPERTYKEY values of the columns to populate. /// The cancellation token. public async Task PopulateTableAsync(IEnumerable columns, CancellationToken cancellationToken) { var columnsToGet = columns.Join(Columns.Cast(), k => k, c => GetColumnPropertyKey(c), (k, c) => c).ToArray(); await PopulateTableAsync(columnsToGet, cancellationToken); } /// Populates the table with all the requested shell items. /// The columns to populate. /// The cancellation token. public async Task PopulateTableAsync(IEnumerable columns, CancellationToken cancellationToken) { var columnsToGet = columns.ToArray(); if (columnsToGet.Except(Columns.Cast()).Any()) throw new ArgumentException("Columns specified that are not in table.", nameof(columnsToGet)); colsToGet = columnsToGet; if (!(parent is null)) { items = parent.IShellFolder.EnumObjects((SHCONTF)itemFilter); } if (items is null && !(parent is null)) { items = parent.IShellFolder.EnumObjects((SHCONTF)itemFilter); } if (Rows.Count > 0) Rows.Clear(); var f2 = parent?.IShellFolder as IShellFolder2; if (!(items is null)) { var slowFetchItems = new List(); var cInfo = columnsToGet.ToLookup(c => IsColumnSlow(c), c => ((PROPERTYKEY)c.ExtendedProperties[extPropKey], c)); var fastCols = cInfo[false].ToList(); var slowCols = cInfo[true].ToList(); foreach (var i in items) { if (cancellationToken.IsCancellationRequested) break; // Add row with fast properties var row = NewRow(); row[colId] = i.GetBytes(); foreach (var (pk, col) in fastCols) row[col] = GetProp(pk, i) ?? DBNull.Value; Rows.Add(row); // If there are slow props, spawn thread to get them if (slowCols.Count > 0) slowFetchItems.Add(GetSlowProps(i, row, slowCols, cancellationToken)); } AllFastRowsAdded?.Invoke(this, EventArgs.Empty); AcceptChanges(); await TaskAgg.WhenAll(slowFetchItems); } TableLoaded?.Invoke(this, EventArgs.Empty); return; object GetProp(in PROPERTYKEY pk, PIDL i) { object o = null; try { if (f2 is null) { using var si = new ShellItem(i); o = si.Properties[pk]; } else { f2.GetDetailsEx(i, pk, out o).ThrowIfFailed(); } } catch { } return o switch { System.Runtime.InteropServices.ComTypes.FILETIME ft => ft.ToDateTime(), _ => o }; } async Task GetSlowProps(PIDL i, DataRow row, IEnumerable<(PROPERTYKEY pk, DataColumn col)> props, CancellationToken cancellationToken) { await TaskAgg.Run(() => { row.BeginEdit(); foreach (var (pk, col) in props) { if (cancellationToken.IsCancellationRequested) break; row[col] = GetProp(pk, i) ?? DBNull.Value; } row.AcceptChanges(); }, cancellationToken); } } /// Refreshes the data table. If columns have not been previously provided, the default columns are used. /// The cancellation token. public async Task RefreshAsync(CancellationToken cancellationToken) => await PopulateTableAsync((IEnumerable)colsToGet ?? DefaultColumns, cancellationToken); private void BuildColumns(ShellFolder folder) { BeginInit(); if (folder.IShellFolder is IShellFolder2 f2) { for (uint i = 0; true; i++) { var hr = f2.GetDefaultColumnState(i, out var cState); if (hr == HRESULT.TYPE_E_OUTOFBOUNDS) break; if (hr.Failed || cState.IsFlagSet(SHCOLSTATE.SHCOLSTATE_HIDDEN)) continue; f2.MapColumnToSCID(i, out var pk); PropertyDescription.TryCreate(pk, out var pd); f2.GetDetailsOf(PIDL.Null, i, out var cDet); var c = new DataColumn(pd?.ToString() ?? cDet.str.ToString(), GetPropType(pd) ?? TypeFromState(cState)) { Caption = pd?.DisplayName, AllowDBNull = true }; SetExtProp(c, pk, cDet.fmt, cState); Columns.Add(c); } } Columns.Add(SetExtProp(new DataColumn(colId, typeof(byte[])))); defCols = new List(Columns.Cast().Where(c => GetColumnState(c).IsFlagSet(SHCOLSTATE.SHCOLSTATE_ONBYDEFAULT))); EndInit(); static Type GetPropType(PropertyDescription pd) { var t = pd?.PropertyType; if (t.Equals(typeof(System.Runtime.InteropServices.ComTypes.FILETIME))) t = typeof(DateTime); return t; } static DataColumn SetExtProp(DataColumn c, in PROPERTYKEY pk = default, ComCtl32.ListViewColumnFormat fmt = ComCtl32.ListViewColumnFormat.LVCFMT_LEFT, SHCOLSTATE state = SHCOLSTATE.SHCOLSTATE_PREFER_VARCMP) { c.ExtendedProperties.Add(extPropKey, pk); c.ExtendedProperties.Add(extAlign, fmt); c.ExtendedProperties.Add(extSlow, state.IsFlagSet(SHCOLSTATE.SHCOLSTATE_SLOW)); c.ExtendedProperties.Add(extState, state); return c; } static Type TypeFromState(SHCOLSTATE st) => (st & SHCOLSTATE.SHCOLSTATE_TYPEMASK) switch { SHCOLSTATE.SHCOLSTATE_TYPE_INT => typeof(int), SHCOLSTATE.SHCOLSTATE_TYPE_DATE => typeof(DateTime), _ => typeof(string), }; } } }