/* * OLVColumn - A column in an ObjectListView * * Author: Phillip Piper * Date: 31-March-2011 5:53 pm * * Change log: * 2015-06-12 JPP - HeaderTextAlign became nullable so that it can be "not set" (this was always the intent) * 2014-09-07 JPP - Added ability to have checkboxes in headers * * 2011-05-27 JPP - Added Sortable, Hideable, Groupable, Searchable, ShowTextInHeader properties * 2011-04-12 JPP - Added HasFilterIndicator * 2011-03-31 JPP - Split into its own file * * Copyright (C) 2011-2014 Phillip Piper * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. */ using System; using System.ComponentModel; using System.Windows.Forms; using System.Drawing; using System.Collections; using System.Diagnostics; using System.Drawing.Design; namespace BrightIdeasSoftware { // TODO //[TypeConverter(typeof(ExpandableObjectConverter))] //public class CheckBoxSettings //{ // private bool useSettings; // private Image checkedImage; // public bool UseSettings { // get { return useSettings; } // set { useSettings = value; } // } // public Image CheckedImage { // get { return checkedImage; } // set { checkedImage = value; } // } // public Image UncheckedImage { // get { return checkedImage; } // set { checkedImage = value; } // } // public Image IndeterminateImage { // get { return checkedImage; } // set { checkedImage = value; } // } //} /// /// An OLVColumn knows which aspect of an object it should present. /// /// /// The column knows how to: /// /// extract its aspect from the row object /// convert an aspect to a string /// calculate the image for the row object /// extract a group "key" from the row object /// convert a group "key" into a title for the group /// /// For sorting to work correctly, aspects from the same column /// must be of the same type, that is, the same aspect cannot sometimes /// return strings and other times integers. /// [Browsable(false)] public partial class OLVColumn : ColumnHeader { /// /// How should the button be sized? /// public enum ButtonSizingMode { /// /// Every cell will have the same sized button, as indicated by ButtonSize property /// FixedBounds, /// /// Every cell will draw a button that fills the cell, inset by ButtonPadding /// CellBounds, /// /// Each button will be resized to contain the text of the Aspect /// TextBounds } #region Life and death /// /// Create an OLVColumn /// public OLVColumn() { } /// /// Initialize a column to have the given title, and show the given aspect /// /// The title of the column /// The aspect to be shown in the column public OLVColumn(string title, string aspect) : this() { this.Text = title; this.AspectName = aspect; } #endregion #region Public Properties /// /// This delegate will be used to extract a value to be displayed in this column. /// /// /// If this is set, AspectName is ignored. /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public AspectGetterDelegate AspectGetter { get { return aspectGetter; } set { aspectGetter = value; } } private AspectGetterDelegate aspectGetter; /// /// Remember if this aspect getter for this column was generated internally, and can therefore /// be regenerated at will /// [Obsolete("This property is no longer maintained", true), Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public bool AspectGetterAutoGenerated { get { return aspectGetterAutoGenerated; } set { aspectGetterAutoGenerated = value; } } private bool aspectGetterAutoGenerated; /// /// The name of the property or method that should be called to get the value to display in this column. /// This is only used if a ValueGetterDelegate has not been given. /// /// This name can be dotted to chain references to properties or parameter-less methods. /// "DateOfBirth" /// "Owner.HomeAddress.Postcode" [Category("ObjectListView"), Description("The name of the property or method that should be called to get the aspect to display in this column"), DefaultValue(null)] public string AspectName { get { return aspectName; } set { aspectName = value; this.aspectMunger = null; } } private string aspectName; /// /// This delegate will be used to put an edited value back into the model object. /// /// /// This does nothing if IsEditable == false. /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public AspectPutterDelegate AspectPutter { get { return aspectPutter; } set { aspectPutter = value; } } private AspectPutterDelegate aspectPutter; /// /// The delegate that will be used to translate the aspect to display in this column into a string. /// /// If this value is set, AspectToStringFormat will be ignored. [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public AspectToStringConverterDelegate AspectToStringConverter { get { return aspectToStringConverter; } set { aspectToStringConverter = value; } } private AspectToStringConverterDelegate aspectToStringConverter; /// /// This format string will be used to convert an aspect to its string representation. /// /// /// This string is passed as the first parameter to the String.Format() method. /// This is only used if AspectToStringConverter has not been set. /// "{0:C}" to convert a number to currency [Category("ObjectListView"), Description("The format string that will be used to convert an aspect to its string representation"), DefaultValue(null)] public string AspectToStringFormat { get { return aspectToStringFormat; } set { aspectToStringFormat = value; } } private string aspectToStringFormat; /// /// Gets or sets whether the cell editor should use AutoComplete /// [Category("ObjectListView"), Description("Should the editor for cells of this column use AutoComplete"), DefaultValue(true)] public bool AutoCompleteEditor { get { return this.AutoCompleteEditorMode != AutoCompleteMode.None; } set { if (value) { if (this.AutoCompleteEditorMode == AutoCompleteMode.None) this.AutoCompleteEditorMode = AutoCompleteMode.Append; } else this.AutoCompleteEditorMode = AutoCompleteMode.None; } } /// /// Gets or sets whether the cell editor should use AutoComplete /// [Category("ObjectListView"), Description("Should the editor for cells of this column use AutoComplete"), DefaultValue(AutoCompleteMode.Append)] public AutoCompleteMode AutoCompleteEditorMode { get { return autoCompleteEditorMode; } set { autoCompleteEditorMode = value; } } private AutoCompleteMode autoCompleteEditorMode = AutoCompleteMode.Append; /// /// Gets whether this column can be hidden by user actions /// /// This take into account both the Hideable property and whether this column /// is the primary column of the listview (column 0). [Browsable(false)] public bool CanBeHidden { get { return this.Hideable && (this.Index != 0); } } /// /// When a cell is edited, should the whole cell be used (minus any space used by checkbox or image)? /// /// /// This is always treated as true when the control is NOT owner drawn. /// /// When this is false (the default) and the control is owner drawn, /// ObjectListView will try to calculate the width of the cell's /// actual contents, and then size the editing control to be just the right width. If this is true, /// the whole width of the cell will be used, regardless of the cell's contents. /// /// If this property is not set on the column, the value from the control will be used /// /// This value is only used when the control is in Details view. /// Regardless of this setting, developers can specify the exact size of the editing control /// by listening for the CellEditStarting event. /// [Category("ObjectListView"), Description("When a cell is edited, should the whole cell be used?"), DefaultValue(null)] public virtual bool? CellEditUseWholeCell { get { return cellEditUseWholeCell; } set { cellEditUseWholeCell = value; } } private bool? cellEditUseWholeCell; /// /// Get whether the whole cell should be used when editing a cell in this column /// /// This calculates the current effective value, which may be different to CellEditUseWholeCell [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public virtual bool CellEditUseWholeCellEffective { get { bool? columnSpecificValue = this.ListView.View == View.Details ? this.CellEditUseWholeCell : (bool?) null; return (columnSpecificValue ?? ((ObjectListView) this.ListView).CellEditUseWholeCell); } } /// /// Gets or sets how many pixels will be left blank around this cells in this column /// /// This setting only takes effect when the control is owner drawn. [Category("ObjectListView"), Description("How many pixels will be left blank around the cells in this column?"), DefaultValue(null)] public Rectangle? CellPadding { get { return this.cellPadding; } set { this.cellPadding = value; } } private Rectangle? cellPadding; /// /// Gets or sets how cells in this column will be vertically aligned. /// /// /// /// This setting only takes effect when the control is owner drawn. /// /// /// If this is not set, the value from the control itself will be used. /// /// [Category("ObjectListView"), Description("How will cell values be vertically aligned?"), DefaultValue(null)] public virtual StringAlignment? CellVerticalAlignment { get { return this.cellVerticalAlignment; } set { this.cellVerticalAlignment = value; } } private StringAlignment? cellVerticalAlignment; /// /// Gets or sets whether this column will show a checkbox. /// /// /// Setting this on column 0 has no effect. Column 0 check box is controlled /// by the CheckBoxes property on the ObjectListView itself. /// [Category("ObjectListView"), Description("Should values in this column be treated as a checkbox, rather than a string?"), DefaultValue(false)] public virtual bool CheckBoxes { get { return checkBoxes; } set { if (this.checkBoxes == value) return; this.checkBoxes = value; if (this.checkBoxes) { if (this.Renderer == null) this.Renderer = new CheckStateRenderer(); } else { if (this.Renderer is CheckStateRenderer) this.Renderer = null; } } } private bool checkBoxes; /// /// Gets or sets the clustering strategy used for this column. /// /// /// /// The clustering strategy is used to build a Filtering menu for this item. /// If this is null, a useful default will be chosen. /// /// /// To disable filtering on this colummn, set UseFiltering to false. /// /// /// Cluster strategies belong to a particular column. The same instance /// cannot be shared between multiple columns. /// /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public IClusteringStrategy ClusteringStrategy { get { if (this.clusteringStrategy == null) this.ClusteringStrategy = this.DecideDefaultClusteringStrategy(); return clusteringStrategy; } set { this.clusteringStrategy = value; if (this.clusteringStrategy != null) this.clusteringStrategy.Column = this; } } private IClusteringStrategy clusteringStrategy; /// /// Gets or sets whether the button in this column (if this column is drawing buttons) will be enabled /// even if the row itself is disabled /// [Category("ObjectListView"), Description("If this column contains a button, should the button be enabled even if the row is disabled?"), DefaultValue(false)] public bool EnableButtonWhenItemIsDisabled { get { return this.enableButtonWhenItemIsDisabled; } set { this.enableButtonWhenItemIsDisabled = value; } } private bool enableButtonWhenItemIsDisabled; /// /// Should this column resize to fill the free space in the listview? /// /// /// /// If you want two (or more) columns to equally share the available free space, set this property to True. /// If you want this column to have a larger or smaller share of the free space, you must /// set the FreeSpaceProportion property explicitly. /// /// /// Space filling columns are still governed by the MinimumWidth and MaximumWidth properties. /// /// /// [Category("ObjectListView"), Description("Will this column resize to fill unoccupied horizontal space in the listview?"), DefaultValue(false)] public bool FillsFreeSpace { get { return this.FreeSpaceProportion > 0; } set { this.FreeSpaceProportion = value ? 1 : 0; } } /// /// What proportion of the unoccupied horizontal space in the control should be given to this column? /// /// /// /// There are situations where it would be nice if a column (normally the rightmost one) would expand as /// the list view expands, so that as much of the column was visible as possible without having to scroll /// horizontally (you should never, ever make your users have to scroll anything horizontally!). /// /// /// A space filling column is resized to occupy a proportion of the unoccupied width of the listview (the /// unoccupied width is the width left over once all the the non-filling columns have been given their space). /// This property indicates the relative proportion of that unoccupied space that will be given to this column. /// The actual value of this property is not important -- only its value relative to the value in other columns. /// For example: /// /// /// If there is only one space filling column, it will be given all the free space, regardless of the value in FreeSpaceProportion. /// /// /// If there are two or more space filling columns and they all have the same value for FreeSpaceProportion, /// they will share the free space equally. /// /// /// If there are three space filling columns with values of 3, 2, and 1 /// for FreeSpaceProportion, then the first column with occupy half the free space, the second will /// occupy one-third of the free space, and the third column one-sixth of the free space. /// /// /// /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public int FreeSpaceProportion { get { return freeSpaceProportion; } set { freeSpaceProportion = Math.Max(0, value); } } private int freeSpaceProportion; /// /// Gets or sets whether groups will be rebuild on this columns values when this column's header is clicked. /// /// /// This setting is only used when ShowGroups is true. /// /// If this is false, clicking the header will not rebuild groups. It will not provide /// any feedback as to why the list is not being regrouped. It is the programmers responsibility to /// provide appropriate feedback. /// /// When this is false, BeforeCreatingGroups events are still fired, which can be used to allow grouping /// or give feedback, on a case by case basis. /// [Category("ObjectListView"), Description("Will the list create groups when this header is clicked?"), DefaultValue(true)] public bool Groupable { get { return groupable; } set { groupable = value; } } private bool groupable = true; /// /// This delegate is called when a group has been created but not yet made /// into a real ListViewGroup. The user can take this opportunity to fill /// in lots of other details about the group. /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public GroupFormatterDelegate GroupFormatter { get { return groupFormatter; } set { groupFormatter = value; } } private GroupFormatterDelegate groupFormatter; /// /// This delegate is called to get the object that is the key for the group /// to which the given row belongs. /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public GroupKeyGetterDelegate GroupKeyGetter { get { return groupKeyGetter; } set { groupKeyGetter = value; } } private GroupKeyGetterDelegate groupKeyGetter; /// /// This delegate is called to convert a group key into a title for that group. /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public GroupKeyToTitleConverterDelegate GroupKeyToTitleConverter { get { return groupKeyToTitleConverter; } set { groupKeyToTitleConverter = value; } } private GroupKeyToTitleConverterDelegate groupKeyToTitleConverter; /// /// When the listview is grouped by this column and group title has an item count, /// how should the lable be formatted? /// /// /// The given format string can/should have two placeholders: /// /// {0} - the original group title /// {1} - the number of items in the group /// /// /// "{0} [{1} items]" [Category("ObjectListView"), Description("The format to use when suffixing item counts to group titles"), DefaultValue(null), Localizable(true)] public string GroupWithItemCountFormat { get { return groupWithItemCountFormat; } set { groupWithItemCountFormat = value; } } private string groupWithItemCountFormat; /// /// Gets this.GroupWithItemCountFormat or a reasonable default /// /// /// If GroupWithItemCountFormat is not set, its value will be taken from the ObjectListView if possible. /// [Browsable(false)] public string GroupWithItemCountFormatOrDefault { get { if (!String.IsNullOrEmpty(this.GroupWithItemCountFormat)) return this.GroupWithItemCountFormat; if (this.ListView != null) { cachedGroupWithItemCountFormat = ((ObjectListView)this.ListView).GroupWithItemCountFormatOrDefault; return cachedGroupWithItemCountFormat; } // There is one rare but pathelogically possible case where the ListView can // be null (if the column is grouping a ListView, but is not one of the columns // for that ListView) so we have to provide a workable default for that rare case. return cachedGroupWithItemCountFormat ?? "{0} [{1} items]"; } } private string cachedGroupWithItemCountFormat; /// /// When the listview is grouped by this column and a group title has an item count, /// how should the lable be formatted if there is only one item in the group? /// /// /// The given format string can/should have two placeholders: /// /// {0} - the original group title /// {1} - the number of items in the group (always 1) /// /// /// "{0} [{1} item]" [Category("ObjectListView"), Description("The format to use when suffixing item counts to group titles"), DefaultValue(null), Localizable(true)] public string GroupWithItemCountSingularFormat { get { return groupWithItemCountSingularFormat; } set { groupWithItemCountSingularFormat = value; } } private string groupWithItemCountSingularFormat; /// /// Get this.GroupWithItemCountSingularFormat or a reasonable default /// /// /// If this value is not set, the values from the list view will be used /// [Browsable(false)] public string GroupWithItemCountSingularFormatOrDefault { get { if (!String.IsNullOrEmpty(this.GroupWithItemCountSingularFormat)) return this.GroupWithItemCountSingularFormat; if (this.ListView != null) { cachedGroupWithItemCountSingularFormat = ((ObjectListView)this.ListView).GroupWithItemCountSingularFormatOrDefault; return cachedGroupWithItemCountSingularFormat; } // There is one rare but pathelogically possible case where the ListView can // be null (if the column is grouping a ListView, but is not one of the columns // for that ListView) so we have to provide a workable default for that rare case. return cachedGroupWithItemCountSingularFormat ?? "{0} [{1} item]"; } } private string cachedGroupWithItemCountSingularFormat; /// /// Gets whether this column should be drawn with a filter indicator in the column header. /// [Browsable(false)] public bool HasFilterIndicator { get { return this.UseFiltering && this.ValuesChosenForFiltering != null && this.ValuesChosenForFiltering.Count > 0; } } /// /// Gets or sets a delegate that will be used to own draw header column. /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public HeaderDrawingDelegate HeaderDrawing { get { return headerDrawing; } set { headerDrawing = value; } } private HeaderDrawingDelegate headerDrawing; /// /// Gets or sets the style that will be used to draw the header for this column /// /// This is only uses when the owning ObjectListView has HeaderUsesThemes set to false. [Category("ObjectListView"), Description("What style will be used to draw the header of this column"), DefaultValue(null)] public HeaderFormatStyle HeaderFormatStyle { get { return this.headerFormatStyle; } set { this.headerFormatStyle = value; } } private HeaderFormatStyle headerFormatStyle; /// /// Gets or sets the font in which the header for this column will be drawn /// /// You should probably use a HeaderFormatStyle instead of this property /// This is only uses when HeaderUsesThemes is false. [Category("ObjectListView"), Description("Which font will be used to draw the header?"), DefaultValue(null)] public Font HeaderFont { get { return this.HeaderFormatStyle == null ? null : this.HeaderFormatStyle.Normal.Font; } set { if (value == null && this.HeaderFormatStyle == null) return; if (this.HeaderFormatStyle == null) this.HeaderFormatStyle = new HeaderFormatStyle(); this.HeaderFormatStyle.SetFont(value); } } /// /// Gets or sets the color in which the text of the header for this column will be drawn /// /// You should probably use a HeaderFormatStyle instead of this property /// This is only uses when HeaderUsesThemes is false. [Category("ObjectListView"), Description("In what color will the header text be drawn?"), DefaultValue(typeof(Color), "")] public Color HeaderForeColor { get { return this.HeaderFormatStyle == null ? Color.Empty : this.HeaderFormatStyle.Normal.ForeColor; } set { if (value.IsEmpty && this.HeaderFormatStyle == null) return; if (this.HeaderFormatStyle == null) this.HeaderFormatStyle = new HeaderFormatStyle(); this.HeaderFormatStyle.SetForeColor(value); } } /// /// Gets or sets whether the text values in this column will act like hyperlinks /// /// This is only taken into account when HeaderUsesThemes is false. [Category("ObjectListView"), Description("Name of the image that will be shown in the column header."), DefaultValue(null), TypeConverter(typeof(ImageKeyConverter)), Editor("System.Windows.Forms.Design.ImageIndexEditor, System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", typeof(UITypeEditor)), RefreshProperties(RefreshProperties.Repaint)] public string HeaderImageKey { get { return headerImageKey; } set { headerImageKey = value; } } private string headerImageKey; /// /// Gets or sets how the text of the header will be drawn? /// [Category("ObjectListView"), Description("How will the header text be aligned? If this is not set, the alignment of the header will follow the alignment of the column"), DefaultValue(null)] public HorizontalAlignment? HeaderTextAlign { get { return headerTextAlign; } set { headerTextAlign = value; } } private HorizontalAlignment? headerTextAlign; /// /// Return the text alignment of the header. This will either have been set explicitly, /// or will follow the alignment of the text in the column /// [Browsable(false)] public HorizontalAlignment HeaderTextAlignOrDefault { get { return headerTextAlign.HasValue ? headerTextAlign.Value : this.TextAlign; } } /// /// Gets the header alignment converted to a StringAlignment /// [Browsable(false)] public StringAlignment HeaderTextAlignAsStringAlignment { get { switch (this.HeaderTextAlignOrDefault) { case HorizontalAlignment.Left: return StringAlignment.Near; case HorizontalAlignment.Center: return StringAlignment.Center; case HorizontalAlignment.Right: return StringAlignment.Far; default: return StringAlignment.Near; } } } /// /// Gets whether or not this column has an image in the header /// [Browsable(false)] public bool HasHeaderImage { get { return (this.ListView != null && this.ListView.SmallImageList != null && this.ListView.SmallImageList.Images.ContainsKey(this.HeaderImageKey)); } } /// /// Gets or sets whether this header will place a checkbox in the header /// [Category("ObjectListView"), Description("Draw a checkbox in the header of this column"), DefaultValue(false)] public bool HeaderCheckBox { get { return headerCheckBox; } set { headerCheckBox = value; } } private bool headerCheckBox; /// /// Gets or sets whether this header will place a tri-state checkbox in the header /// [Category("ObjectListView"), Description("Draw a tri-state checkbox in the header of this column"), DefaultValue(false)] public bool HeaderTriStateCheckBox { get { return headerTriStateCheckBox; } set { headerTriStateCheckBox = value; } } private bool headerTriStateCheckBox; /// /// Gets or sets the checkedness of the checkbox in the header of this column /// [Category("ObjectListView"), Description("Checkedness of the header checkbox"), DefaultValue(CheckState.Unchecked)] public CheckState HeaderCheckState { get { return headerCheckState; } set { headerCheckState = value; } } private CheckState headerCheckState = CheckState.Unchecked; /// /// Gets or sets whether the /// checking/unchecking the value of the header's checkbox will result in the /// checkboxes for all cells in this column being set to the same checked/unchecked. /// Defaults to true. /// /// /// /// There is no reverse of this function that automatically updates the header when the /// checkedness of a cell changes. /// /// /// This property's behaviour on a TreeListView is probably best describes as undefined /// and should be avoided. /// /// /// The performance of this action (checking/unchecking all rows) is O(n) where n is the /// number of rows. It will work on large virtual lists, but it may take some time. /// /// [Category("ObjectListView"), Description("Update row checkboxs when the header checkbox is clicked by the user"), DefaultValue(true)] public bool HeaderCheckBoxUpdatesRowCheckBoxes { get { return headerCheckBoxUpdatesRowCheckBoxes; } set { headerCheckBoxUpdatesRowCheckBoxes = value; } } private bool headerCheckBoxUpdatesRowCheckBoxes = true; /// /// Gets or sets whether the checkbox in the header is disabled /// /// /// Clicking on a disabled checkbox does not change its value, though it does raise /// a HeaderCheckBoxChanging event, which allows the programmer the opportunity to do /// something appropriate. [Category("ObjectListView"), Description("Is the checkbox in the header of this column disabled"), DefaultValue(false)] public bool HeaderCheckBoxDisabled { get { return headerCheckBoxDisabled; } set { headerCheckBoxDisabled = value; } } private bool headerCheckBoxDisabled; /// /// Gets or sets whether this column can be hidden by the user. /// /// /// Column 0 can never be hidden, regardless of this setting. /// [Category("ObjectListView"), Description("Will the user be able to choose to hide this column?"), DefaultValue(true)] public bool Hideable { get { return hideable; } set { hideable = value; } } private bool hideable = true; /// /// Gets or sets whether the text values in this column will act like hyperlinks /// [Category("ObjectListView"), Description("Will the text values in the cells of this column act like hyperlinks?"), DefaultValue(false)] public bool Hyperlink { get { return hyperlink; } set { hyperlink = value; } } private bool hyperlink; /// /// This is the name of property that will be invoked to get the image selector of the /// image that should be shown in this column. /// It can return an int, string, Image or null. /// /// /// This is ignored if ImageGetter is not null. /// The property can use these return value to identify the image: /// /// null or -1 -- indicates no image /// an int -- the int value will be used as an index into the image list /// a String -- the string value will be used as a key into the image list /// an Image -- the Image will be drawn directly (only in OwnerDrawn mode) /// /// [Category("ObjectListView"), Description("The name of the property that holds the image selector"), DefaultValue(null)] public string ImageAspectName { get { return imageAspectName; } set { imageAspectName = value; } } private string imageAspectName; /// /// This delegate is called to get the image selector of the image that should be shown in this column. /// It can return an int, string, Image or null. /// /// This delegate can use these return value to identify the image: /// /// null or -1 -- indicates no image /// an int -- the int value will be used as an index into the image list /// a String -- the string value will be used as a key into the image list /// an Image -- the Image will be drawn directly (only in OwnerDrawn mode) /// /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public ImageGetterDelegate ImageGetter { get { return imageGetter; } set { imageGetter = value; } } private ImageGetterDelegate imageGetter; /// /// Gets or sets whether this column will draw buttons in its cells /// /// /// /// When this is set to true, the renderer for the column is become a ColumnButtonRenderer /// if it isn't already. If this is set to false, any previous button renderer will be discarded /// /// If the cell's aspect is null or empty, nothing will be drawn in the cell. [Category("ObjectListView"), Description("Does this column draw its cells as buttons?"), DefaultValue(false)] public bool IsButton { get { return isButton; } set { isButton = value; if (value) { ColumnButtonRenderer buttonRenderer = this.Renderer as ColumnButtonRenderer; if (buttonRenderer == null) { this.Renderer = this.CreateColumnButtonRenderer(); this.FillInColumnButtonRenderer(); } } else { if (this.Renderer is ColumnButtonRenderer) this.Renderer = null; } } } private bool isButton; /// /// Create a ColumnButtonRenderer to draw buttons in this column /// /// protected virtual ColumnButtonRenderer CreateColumnButtonRenderer() { return new ColumnButtonRenderer(); } /// /// Fill in details to our ColumnButtonRenderer based on the properties set on the column /// protected virtual void FillInColumnButtonRenderer() { ColumnButtonRenderer buttonRenderer = this.Renderer as ColumnButtonRenderer; if (buttonRenderer == null) return; buttonRenderer.SizingMode = this.ButtonSizing; buttonRenderer.ButtonSize = this.ButtonSize; buttonRenderer.ButtonPadding = this.ButtonPadding; buttonRenderer.MaxButtonWidth = this.ButtonMaxWidth; } /// /// Gets or sets the maximum width that a button can occupy. /// -1 means there is no maximum width. /// /// This is only considered when the SizingMode is TextBounds [Category("ObjectListView"), Description("The maximum width that a button can occupy when the SizingMode is TextBounds"), DefaultValue(-1)] public int ButtonMaxWidth { get { return this.buttonMaxWidth; } set { this.buttonMaxWidth = value; FillInColumnButtonRenderer(); } } private int buttonMaxWidth = -1; /// /// Gets or sets the extra space that surrounds the cell when the SizingMode is TextBounds /// [Category("ObjectListView"), Description("The extra space that surrounds the cell when the SizingMode is TextBounds"), DefaultValue(null)] public Size? ButtonPadding { get { return this.buttonPadding; } set { this.buttonPadding = value; this.FillInColumnButtonRenderer(); } } private Size? buttonPadding; /// /// Gets or sets the size of the button when the SizingMode is FixedBounds /// /// If this is not set, the bounds of the cell will be used [Category("ObjectListView"), Description("The size of the button when the SizingMode is FixedBounds"), DefaultValue(null)] public Size? ButtonSize { get { return this.buttonSize; } set { this.buttonSize = value; this.FillInColumnButtonRenderer(); } } private Size? buttonSize; /// /// Gets or sets how each button will be sized if this column is displaying buttons /// [Category("ObjectListView"), Description("If this column is showing buttons, how each button will be sized"), DefaultValue(ButtonSizingMode.TextBounds)] public ButtonSizingMode ButtonSizing { get { return this.buttonSizing; } set { this.buttonSizing = value; this.FillInColumnButtonRenderer(); } } private ButtonSizingMode buttonSizing = ButtonSizingMode.TextBounds; /// /// Can the values shown in this column be edited? /// /// This defaults to true, since the primary means to control the editability of a listview /// is on the listview itself. Once a listview is editable, all the columns are too, unless the /// programmer explicitly marks them as not editable [Category("ObjectListView"), Description("Can the value in this column be edited?"), DefaultValue(true)] public bool IsEditable { get { return isEditable; } set { isEditable = value; } } private bool isEditable = true; /// /// Is this column a fixed width column? /// [Browsable(false)] public bool IsFixedWidth { get { return (this.MinimumWidth != -1 && this.MaximumWidth != -1 && this.MinimumWidth >= this.MaximumWidth); } } /// /// Get/set whether this column should be used when the view is switched to tile view. /// /// Column 0 is always included in tileview regardless of this setting. /// Tile views do not work well with many "columns" of information. /// Two or three works best. [Category("ObjectListView"), Description("Will this column be used when the view is switched to tile view"), DefaultValue(false)] public bool IsTileViewColumn { get { return isTileViewColumn; } set { isTileViewColumn = value; } } private bool isTileViewColumn; /// /// Gets or sets whether the text of this header should be rendered vertically. /// /// /// If this is true, it is a good idea to set ToolTipText to the name of the column so it's easy to read. /// Vertical headers are text only. They do not draw their image. /// [Category("ObjectListView"), Description("Will the header for this column be drawn vertically?"), DefaultValue(false)] public bool IsHeaderVertical { get { return isHeaderVertical; } set { isHeaderVertical = value; } } private bool isHeaderVertical; /// /// Can this column be seen by the user? /// /// After changing this value, you must call RebuildColumns() before the changes will take effect. [Category("ObjectListView"), Description("Can this column be seen by the user?"), DefaultValue(true)] public bool IsVisible { get { return isVisible; } set { if (isVisible == value) return; isVisible = value; OnVisibilityChanged(EventArgs.Empty); } } private bool isVisible = true; /// /// Where was this column last positioned within the Detail view columns /// /// DisplayIndex is volatile. Once a column is removed from the control, /// there is no way to discover where it was in the display order. This property /// guards that information even when the column is not in the listview's active columns. [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public int LastDisplayIndex { get { return this.lastDisplayIndex; } set { this.lastDisplayIndex = value; } } private int lastDisplayIndex = -1; /// /// What is the maximum width that the user can give to this column? /// /// -1 means there is no maximum width. Give this the same value as MinimumWidth to make a fixed width column. [Category("ObjectListView"), Description("What is the maximum width to which the user can resize this column? -1 means no limit"), DefaultValue(-1)] public int MaximumWidth { get { return maxWidth; } set { maxWidth = value; if (maxWidth != -1 && this.Width > maxWidth) this.Width = maxWidth; } } private int maxWidth = -1; /// /// What is the minimum width that the user can give to this column? /// /// -1 means there is no minimum width. Give this the same value as MaximumWidth to make a fixed width column. [Category("ObjectListView"), Description("What is the minimum width to which the user can resize this column? -1 means no limit"), DefaultValue(-1)] public int MinimumWidth { get { return minWidth; } set { minWidth = value; if (this.Width < minWidth) this.Width = minWidth; } } private int minWidth = -1; /// /// Get/set the renderer that will be invoked when a cell needs to be redrawn /// [Category("ObjectListView"), Description("The renderer will draw this column when the ListView is owner drawn"), DefaultValue(null)] public IRenderer Renderer { get { return renderer; } set { renderer = value; } } private IRenderer renderer; /// /// This delegate is called when a cell needs to be drawn in OwnerDrawn mode. /// /// This method is kept primarily for backwards compatibility. /// New code should implement an IRenderer, though this property will be maintained. [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public RenderDelegate RendererDelegate { get { Version1Renderer version1Renderer = this.Renderer as Version1Renderer; return version1Renderer != null ? version1Renderer.RenderDelegate : null; } set { this.Renderer = value == null ? null : new Version1Renderer(value); } } /// /// Gets or sets whether the text in this column's cell will be used when doing text searching. /// /// /// /// If this is false, text filters will not trying searching this columns cells when looking for matches. /// /// [Category("ObjectListView"), Description("Will the text of the cells in this column be considered when searching?"), DefaultValue(true)] public bool Searchable { get { return searchable; } set { searchable = value; } } private bool searchable = true; /// /// Gets or sets a delegate which will return the array of text values that should be /// considered for text matching when using a text based filter. /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public SearchValueGetterDelegate SearchValueGetter { get { return searchValueGetter; } set { searchValueGetter = value; } } private SearchValueGetterDelegate searchValueGetter; /// /// Gets or sets whether the header for this column will include the column's Text. /// /// /// /// If this is false, the only thing rendered in the column header will be the image from . /// /// This setting is only considered when is false on the owning ObjectListView. /// [Category("ObjectListView"), Description("Will the header for this column include text?"), DefaultValue(true)] public bool ShowTextInHeader { get { return showTextInHeader; } set { showTextInHeader = value; } } private bool showTextInHeader = true; /// /// Gets or sets whether the contents of the list will be resorted when the user clicks the /// header of this column. /// /// /// /// If this is false, clicking the header will not sort the list, but will not provide /// any feedback as to why the list is not being sorted. It is the programmers responsibility to /// provide appropriate feedback. /// /// When this is false, BeforeSorting events are still fired, which can be used to allow sorting /// or give feedback, on a case by case basis. /// [Category("ObjectListView"), Description("Will clicking this columns header resort the list?"), DefaultValue(true)] public bool Sortable { get { return sortable; } set { sortable = value; } } private bool sortable = true; /// /// Gets or sets the horizontal alignment of the contents of the column. /// /// .NET will not allow column 0 to have any alignment except /// to the left. We can't change the basic behaviour of the listview, /// but when owner drawn, column 0 can now have other alignments. new public HorizontalAlignment TextAlign { get { return this.textAlign.HasValue ? this.textAlign.Value : base.TextAlign; } set { this.textAlign = value; base.TextAlign = value; } } private HorizontalAlignment? textAlign; /// /// Gets the StringAlignment equivilent of the column text alignment /// [Browsable(false)] public StringAlignment TextStringAlign { get { switch (this.TextAlign) { case HorizontalAlignment.Center: return StringAlignment.Center; case HorizontalAlignment.Left: return StringAlignment.Near; case HorizontalAlignment.Right: return StringAlignment.Far; default: return StringAlignment.Near; } } } /// /// What string should be displayed when the mouse is hovered over the header of this column? /// /// If a HeaderToolTipGetter is installed on the owning ObjectListView, this /// value will be ignored. [Category("ObjectListView"), Description("The tooltip to show when the mouse is hovered over the header of this column"), DefaultValue((String)null), Localizable(true)] public String ToolTipText { get { return toolTipText; } set { toolTipText = value; } } private String toolTipText; /// /// Should this column have a tri-state checkbox? /// /// /// If this is true, the user can choose the third state (normally Indeterminate). /// [Category("ObjectListView"), Description("Should values in this column be treated as a tri-state checkbox?"), DefaultValue(false)] public virtual bool TriStateCheckBoxes { get { return triStateCheckBoxes; } set { triStateCheckBoxes = value; if (value && !this.CheckBoxes) this.CheckBoxes = true; } } private bool triStateCheckBoxes; /// /// Group objects by the initial letter of the aspect of the column /// /// /// One common pattern is to group column by the initial letter of the value for that group. /// The aspect must be a string (obviously). /// [Category("ObjectListView"), Description("The name of the property or method that should be called to get the aspect to display in this column"), DefaultValue(false)] public bool UseInitialLetterForGroup { get { return useInitialLetterForGroup; } set { useInitialLetterForGroup = value; } } private bool useInitialLetterForGroup; /// /// Gets or sets whether or not this column should be user filterable /// [Category("ObjectListView"), Description("Does this column want to show a Filter menu item when its header is right clicked"), DefaultValue(true)] public bool UseFiltering { get { return useFiltering; } set { useFiltering = value; } } private bool useFiltering = true; /// /// Gets or sets a filter that will only include models where the model's value /// for this column is one of the values in ValuesChosenForFiltering /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public IModelFilter ValueBasedFilter { get { if (!this.UseFiltering) return null; if (valueBasedFilter != null) return valueBasedFilter; if (this.ClusteringStrategy == null) return null; if (this.ValuesChosenForFiltering == null || this.ValuesChosenForFiltering.Count == 0) return null; return this.ClusteringStrategy.CreateFilter(this.ValuesChosenForFiltering); } set { valueBasedFilter = value; } } private IModelFilter valueBasedFilter; /// /// Gets or sets the values that will be used to generate a filter for this /// column. For a model to be included by the generated filter, its value for this column /// must be in this list. If the list is null or empty, this column will /// not be used for filtering. /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public IList ValuesChosenForFiltering { get { return this.valuesChosenForFiltering; } set { this.valuesChosenForFiltering = value; } } private IList valuesChosenForFiltering = new ArrayList(); /// /// What is the width of this column? /// [Category("ObjectListView"), Description("The width in pixels of this column"), DefaultValue(60)] new public int Width { get { return base.Width; } set { if (this.MaximumWidth != -1 && value > this.MaximumWidth) base.Width = this.MaximumWidth; else base.Width = Math.Max(this.MinimumWidth, value); } } /// /// Gets or set whether the contents of this column's cells should be word wrapped /// /// If this column uses a custom IRenderer (that is, one that is not descended /// from BaseRenderer), then that renderer is responsible for implementing word wrapping. [Category("ObjectListView"), Description("Draw this column cell's word wrapped"), DefaultValue(false)] public bool WordWrap { get { return wordWrap; } set { wordWrap = value; // If there isn't a renderer and they are turning word wrap off, we don't need to do anything if (this.Renderer == null && !wordWrap) return; // All other cases require a renderer of some sort if (this.Renderer == null) this.Renderer = new HighlightTextRenderer(); BaseRenderer baseRenderer = this.Renderer as BaseRenderer; // If there is a custom renderer (not descended from BaseRenderer), // we leave it up to them to implement wrapping if (baseRenderer == null) return; baseRenderer.CanWrap = wordWrap; } } private bool wordWrap; #endregion #region Object commands /// /// For a given group value, return the string that should be used as the groups title. /// /// The group key that is being converted to a title /// string public string ConvertGroupKeyToTitle(object value) { if (this.groupKeyToTitleConverter != null) return this.groupKeyToTitleConverter(value); return value == null ? ObjectListView.GroupTitleDefault : this.ValueToString(value); } /// /// Get the checkedness of the given object for this column /// /// The row object that is being displayed /// The checkedness of the object public CheckState GetCheckState(object rowObject) { if (!this.CheckBoxes) return CheckState.Unchecked; bool? aspectAsBool = this.GetValue(rowObject) as bool?; if (aspectAsBool.HasValue) { if (aspectAsBool.Value) return CheckState.Checked; else return CheckState.Unchecked; } else return CheckState.Indeterminate; } /// /// Put the checkedness of the given object for this column /// /// The row object that is being displayed /// /// The checkedness of the object public void PutCheckState(object rowObject, CheckState newState) { if (newState == CheckState.Checked) this.PutValue(rowObject, true); else if (newState == CheckState.Unchecked) this.PutValue(rowObject, false); else this.PutValue(rowObject, null); } /// /// For a given row object, extract the value indicated by the AspectName property of this column. /// /// The row object that is being displayed /// An object, which is the aspect named by AspectName public object GetAspectByName(object rowObject) { if (this.aspectMunger == null) this.aspectMunger = new Munger(this.AspectName); return this.aspectMunger.GetValue(rowObject); } private Munger aspectMunger; /// /// For a given row object, return the object that is the key of the group that this row belongs to. /// /// The row object that is being displayed /// Group key object public object GetGroupKey(object rowObject) { if (this.groupKeyGetter != null) return this.groupKeyGetter(rowObject); object key = this.GetValue(rowObject); if (this.UseInitialLetterForGroup) { String keyAsString = key as String; if (!String.IsNullOrEmpty(keyAsString)) return keyAsString.Substring(0, 1).ToUpper(); } return key; } /// /// For a given row object, return the image selector of the image that should displayed in this column. /// /// The row object that is being displayed /// int or string or Image. int or string will be used as index into image list. null or -1 means no image public Object GetImage(object rowObject) { if (this.CheckBoxes) return this.GetCheckStateImage(rowObject); if (this.ImageGetter != null) return this.ImageGetter(rowObject); if (!String.IsNullOrEmpty(this.ImageAspectName)) { if (this.imageAspectMunger == null) this.imageAspectMunger = new Munger(this.ImageAspectName); return this.imageAspectMunger.GetValue(rowObject); } // I think this is wrong. ImageKey is meant for the image in the header, not in the rows if (!String.IsNullOrEmpty(this.ImageKey)) return this.ImageKey; return this.ImageIndex; } private Munger imageAspectMunger; /// /// Return the image that represents the check box for the given model /// /// /// public string GetCheckStateImage(Object rowObject) { CheckState checkState = this.GetCheckState(rowObject); if (checkState == CheckState.Checked) return ObjectListView.CHECKED_KEY; if (checkState == CheckState.Unchecked) return ObjectListView.UNCHECKED_KEY; return ObjectListView.INDETERMINATE_KEY; } /// /// For a given row object, return the strings that will be searched when trying to filter by string. /// /// /// This will normally be the simple GetStringValue result, but if this column is non-textual (e.g. image) /// you might want to install a SearchValueGetter delegate which can return something that could be used /// for text filtering. /// /// /// The array of texts to be searched. If this returns null, search will not match that object. public string[] GetSearchValues(object rowObject) { if (this.SearchValueGetter != null) return this.SearchValueGetter(rowObject); var stringValue = this.GetStringValue(rowObject); DescribedTaskRenderer dtr = this.Renderer as DescribedTaskRenderer; if (dtr != null) { return new string[] { stringValue, dtr.GetDescription(rowObject) }; } return new string[] { stringValue }; } /// /// For a given row object, return the string representation of the value shown in this column. /// /// /// For aspects that are string (e.g. aPerson.Name), the aspect and its string representation are the same. /// For non-strings (e.g. aPerson.DateOfBirth), the string representation is very different. /// /// /// public string GetStringValue(object rowObject) { return this.ValueToString(this.GetValue(rowObject)); } /// /// For a given row object, return the object that is to be displayed in this column. /// /// The row object that is being displayed /// An object, which is the aspect to be displayed public object GetValue(object rowObject) { if (this.AspectGetter == null) return this.GetAspectByName(rowObject); else return this.AspectGetter(rowObject); } /// /// Update the given model object with the given value using the column's /// AspectName. /// /// The model object to be updated /// The value to be put into the model public void PutAspectByName(Object rowObject, Object newValue) { if (this.aspectMunger == null) this.aspectMunger = new Munger(this.AspectName); this.aspectMunger.PutValue(rowObject, newValue); } /// /// Update the given model object with the given value /// /// The model object to be updated /// The value to be put into the model public void PutValue(Object rowObject, Object newValue) { if (this.aspectPutter == null) this.PutAspectByName(rowObject, newValue); else this.aspectPutter(rowObject, newValue); } /// /// Convert the aspect object to its string representation. /// /// /// If the column has been given a AspectToStringConverter, that will be used to do /// the conversion, otherwise just use ToString(). /// The returned value will not be null. Nulls are always converted /// to empty strings. /// /// The value of the aspect that should be displayed /// A string representation of the aspect public string ValueToString(object value) { // Give the installed converter a chance to work (even if the value is null) if (this.AspectToStringConverter != null) return this.AspectToStringConverter(value) ?? String.Empty; // Without a converter, nulls become simple empty strings if (value == null) return String.Empty; string fmt = this.AspectToStringFormat; if (String.IsNullOrEmpty(fmt)) return value.ToString(); else return String.Format(fmt, value); } #endregion #region Utilities /// /// Decide the clustering strategy that will be used for this column /// /// private IClusteringStrategy DecideDefaultClusteringStrategy() { if (!this.UseFiltering) return null; if (this.DataType == typeof(DateTime)) return new DateTimeClusteringStrategy(); return new ClustersFromGroupsStrategy(); } /// /// Gets or sets the type of data shown in this column. /// /// If this is not set, it will try to get the type /// by looking through the rows of the listview. [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public Type DataType { get { if (this.dataType == null) { ObjectListView olv = this.ListView as ObjectListView; if (olv != null) { object value = olv.GetFirstNonNullValue(this); if (value != null) return value.GetType(); // THINK: Should we cache this? } } return this.dataType; } set { this.dataType = value; } } private Type dataType; #region Events /// /// This event is triggered when the visibility of this column changes. /// [Category("ObjectListView"), Description("This event is triggered when the visibility of the column changes.")] public event EventHandler VisibilityChanged; /// /// Tell the world when visibility of a column changes. /// public virtual void OnVisibilityChanged(EventArgs e) { if (this.VisibilityChanged != null) this.VisibilityChanged(this, e); } #endregion /// /// Create groupies /// This is an untyped version to help with Generator and OLVColumn attributes /// /// /// public void MakeGroupies(object[] values, string[] descriptions) { this.MakeGroupies(values, descriptions, null, null, null); } /// /// Create groupies /// /// /// /// public void MakeGroupies(T[] values, string[] descriptions) { this.MakeGroupies(values, descriptions, null, null, null); } /// /// Create groupies /// /// /// /// /// public void MakeGroupies(T[] values, string[] descriptions, object[] images) { this.MakeGroupies(values, descriptions, images, null, null); } /// /// Create groupies /// /// /// /// /// /// public void MakeGroupies(T[] values, string[] descriptions, object[] images, string[] subtitles) { this.MakeGroupies(values, descriptions, images, subtitles, null); } /// /// Create groupies. /// Install delegates that will group the columns aspects into progressive partitions. /// If an aspect is less than value[n], it will be grouped with description[n]. /// If an aspect has a value greater than the last element in "values", it will be grouped /// with the last element in "descriptions". /// /// Array of values. Values must be able to be /// compared to the aspect (using IComparable) /// The description for the matching value. The last element is the default description. /// If there are n values, there must be n+1 descriptions. /// /// this.salaryColumn.MakeGroupies( /// new UInt32[] { 20000, 100000 }, /// new string[] { "Lowly worker", "Middle management", "Rarified elevation"}); /// /// /// /// /// public void MakeGroupies(T[] values, string[] descriptions, object[] images, string[] subtitles, string[] tasks) { // Sanity checks if (values == null) throw new ArgumentNullException("values"); if (descriptions == null) throw new ArgumentNullException("descriptions"); if (values.Length + 1 != descriptions.Length) throw new ArgumentException("descriptions must have one more element than values."); // Install a delegate that returns the index of the description to be shown this.GroupKeyGetter = delegate(object row) { Object aspect = this.GetValue(row); if (aspect == null || aspect == DBNull.Value) return -1; IComparable comparable = (IComparable)aspect; for (int i = 0; i < values.Length; i++) { if (comparable.CompareTo(values[i]) < 0) return i; } // Display the last element in the array return descriptions.Length - 1; }; // Install a delegate that simply looks up the given index in the descriptions. this.GroupKeyToTitleConverter = delegate(object key) { if ((int)key < 0) return ""; return descriptions[(int)key]; }; // Install one delegate that does all the other formatting this.GroupFormatter = delegate(OLVGroup group, GroupingParameters parms) { int key = (int)group.Key; // we know this is an int since we created it in GroupKeyGetter if (key >= 0) { if (images != null && key < images.Length) group.TitleImage = images[key]; if (subtitles != null && key < subtitles.Length) group.Subtitle = subtitles[key]; if (tasks != null && key < tasks.Length) group.Task = tasks[key]; } }; } /// /// Create groupies based on exact value matches. /// /// /// Install delegates that will group rows into partitions based on equality of this columns aspects. /// If an aspect is equal to value[n], it will be grouped with description[n]. /// If an aspect is not equal to any value, it will be grouped with "[other]". /// /// Array of values. Values must be able to be /// equated to the aspect /// The description for the matching value. /// /// this.marriedColumn.MakeEqualGroupies( /// new MaritalStatus[] { MaritalStatus.Single, MaritalStatus.Married, MaritalStatus.Divorced, MaritalStatus.Partnered }, /// new string[] { "Looking", "Content", "Looking again", "Mostly content" }); /// /// /// /// /// public void MakeEqualGroupies(T[] values, string[] descriptions, object[] images, string[] subtitles, string[] tasks) { // Sanity checks if (values == null) throw new ArgumentNullException("values"); if (descriptions == null) throw new ArgumentNullException("descriptions"); if (values.Length != descriptions.Length) throw new ArgumentException("descriptions must have the same number of elements as values."); ArrayList valuesArray = new ArrayList(values); // Install a delegate that returns the index of the description to be shown this.GroupKeyGetter = delegate(object row) { return valuesArray.IndexOf(this.GetValue(row)); }; // Install a delegate that simply looks up the given index in the descriptions. this.GroupKeyToTitleConverter = delegate(object key) { int intKey = (int)key; // we know this is an int since we created it in GroupKeyGetter return (intKey < 0) ? "[other]" : descriptions[intKey]; }; // Install one delegate that does all the other formatting this.GroupFormatter = delegate(OLVGroup group, GroupingParameters parms) { int key = (int)group.Key; // we know this is an int since we created it in GroupKeyGetter if (key >= 0) { if (images != null && key < images.Length) group.TitleImage = images[key]; if (subtitles != null && key < subtitles.Length) group.Subtitle = subtitles[key]; if (tasks != null && key < tasks.Length) group.Task = tasks[key]; } }; } #endregion } }