Imported existing sources

This commit is contained in:
Daniel Brunner
2017-02-04 13:47:58 +01:00
parent 14a5be61cc
commit 7c97b9e539
86 changed files with 49665 additions and 0 deletions
+125
View File
@@ -0,0 +1,125 @@
/*
* Cluster - Implements a simple cluster
*
* Author: Phillip Piper
* Date: 3-March-2011 10:53 pm
*
* Change log:
* 2011-03-03 JPP - First version
*
* 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 <http://www.gnu.org/licenses/>.
*
* If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com.
*/
using System;
using System.Collections.Generic;
using System.Text;
namespace BrightIdeasSoftware {
/// <summary>
/// Concrete implementation of the ICluster interface.
/// </summary>
public class Cluster : ICluster {
#region Life and death
/// <summary>
/// Create a cluster
/// </summary>
/// <param name="key">The key for the cluster</param>
public Cluster(object key) {
this.Count = 1;
this.ClusterKey = key;
}
#endregion
#region Public overrides
/// <summary>
/// Return a string representation of this cluster
/// </summary>
/// <returns></returns>
public override string ToString() {
return this.DisplayLabel ?? "[empty]";
}
#endregion
#region Implementation of ICluster
/// <summary>
/// Gets or sets how many items belong to this cluster
/// </summary>
public int Count {
get { return count; }
set { count = value; }
}
private int count;
/// <summary>
/// Gets or sets the label that will be shown to the user to represent
/// this cluster
/// </summary>
public string DisplayLabel {
get { return displayLabel; }
set { displayLabel = value; }
}
private string displayLabel;
/// <summary>
/// Gets or sets the actual data object that all members of this cluster
/// have commonly returned.
/// </summary>
public object ClusterKey {
get { return clusterKey; }
set { clusterKey = value; }
}
private object clusterKey;
#endregion
#region Implementation of IComparable
/// <summary>
/// Return an indication of the ordering between this object and the given one
/// </summary>
/// <param name="other"></param>
/// <returns></returns>
public int CompareTo(object other) {
if (other == null || other == System.DBNull.Value)
return 1;
ICluster otherCluster = other as ICluster;
if (otherCluster == null)
return 1;
string keyAsString = this.ClusterKey as string;
if (keyAsString != null)
return String.Compare(keyAsString, otherCluster.ClusterKey as string, StringComparison.CurrentCultureIgnoreCase);
IComparable keyAsComparable = this.ClusterKey as IComparable;
if (keyAsComparable != null)
return keyAsComparable.CompareTo(otherCluster.ClusterKey);
return -1;
}
#endregion
}
}
+189
View File
@@ -0,0 +1,189 @@
/*
* ClusteringStrategy - Implements a simple clustering strategy
*
* Author: Phillip Piper
* Date: 3-March-2011 10:53 pm
*
* Change log:
* 2011-03-03 JPP - First version
*
* 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 <http://www.gnu.org/licenses/>.
*
* If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com.
*/
using System;
using System.Collections;
using System.Text;
namespace BrightIdeasSoftware {
/// <summary>
/// This class provides a useful base implemention of a clustering
/// strategy where the clusters are grouped around the value of a given column.
/// </summary>
public class ClusteringStrategy : IClusteringStrategy {
#region Static properties
/// <summary>
/// This field is the text that will be shown to the user when a cluster
/// key is null. It is exposed so it can be localized.
/// </summary>
static public string NULL_LABEL = "[null]";
/// <summary>
/// This field is the text that will be shown to the user when a cluster
/// key is empty (i.e. a string of zero length). It is exposed so it can be localized.
/// </summary>
static public string EMPTY_LABEL = "[empty]";
/// <summary>
/// Gets or sets the format that will be used by default for clusters that only
/// contain 1 item. The format string must accept two placeholders:
/// - {0} is the cluster key converted to a string
/// - {1} is the number of items in the cluster (always 1 in this case)
/// </summary>
static public string DefaultDisplayLabelFormatSingular {
get { return defaultDisplayLabelFormatSingular; }
set { defaultDisplayLabelFormatSingular = value; }
}
static private string defaultDisplayLabelFormatSingular = "{0} ({1} item)";
/// <summary>
/// Gets or sets the format that will be used by default for clusters that
/// contain 0 or two or more items. The format string must accept two placeholders:
/// - {0} is the cluster key converted to a string
/// - {1} is the number of items in the cluster
/// </summary>
static public string DefaultDisplayLabelFormatPlural {
get { return defaultDisplayLabelFormatPural; }
set { defaultDisplayLabelFormatPural = value; }
}
static private string defaultDisplayLabelFormatPural = "{0} ({1} items)";
#endregion
#region Life and death
/// <summary>
/// Create a clustering strategy
/// </summary>
public ClusteringStrategy() {
this.DisplayLabelFormatSingular = DefaultDisplayLabelFormatSingular;
this.DisplayLabelFormatPlural = DefaultDisplayLabelFormatPlural;
}
#endregion
#region Public properties
/// <summary>
/// Gets or sets the column upon which this strategy is operating
/// </summary>
public OLVColumn Column {
get { return column; }
set { column = value; }
}
private OLVColumn column;
/// <summary>
/// Gets or sets the format that will be used when the cluster
/// contains only 1 item. The format string must accept two placeholders:
/// - {0} is the cluster key converted to a string
/// - {1} is the number of items in the cluster (always 1 in this case)
/// </summary>
/// <remarks>If this is not set, the value from
/// ClusteringStrategy.DefaultDisplayLabelFormatSingular will be used</remarks>
public string DisplayLabelFormatSingular {
get { return displayLabelFormatSingular; }
set { displayLabelFormatSingular = value; }
}
private string displayLabelFormatSingular;
/// <summary>
/// Gets or sets the format that will be used when the cluster
/// contains 0 or two or more items. The format string must accept two placeholders:
/// - {0} is the cluster key converted to a string
/// - {1} is the number of items in the cluster
/// </summary>
/// <remarks>If this is not set, the value from
/// ClusteringStrategy.DefaultDisplayLabelFormatPlural will be used</remarks>
public string DisplayLabelFormatPlural {
get { return displayLabelFormatPural; }
set { displayLabelFormatPural = value; }
}
private string displayLabelFormatPural;
#endregion
#region ICluster implementation
/// <summary>
/// Get the cluster key by which the given model will be partitioned by this strategy
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
virtual public object GetClusterKey(object model) {
return this.Column.GetValue(model);
}
/// <summary>
/// Create a cluster to hold the given cluster key
/// </summary>
/// <param name="clusterKey"></param>
/// <returns></returns>
virtual public ICluster CreateCluster(object clusterKey) {
return new Cluster(clusterKey);
}
/// <summary>
/// Gets the display label that the given cluster should use
/// </summary>
/// <param name="cluster"></param>
/// <returns></returns>
virtual public string GetClusterDisplayLabel(ICluster cluster) {
string s = this.Column.ValueToString(cluster.ClusterKey) ?? NULL_LABEL;
if (String.IsNullOrEmpty(s))
s = EMPTY_LABEL;
return this.ApplyDisplayFormat(cluster, s);
}
/// <summary>
/// Create a filter that will include only model objects that
/// match one or more of the given values.
/// </summary>
/// <param name="valuesChosenForFiltering"></param>
/// <returns></returns>
virtual public IModelFilter CreateFilter(IList valuesChosenForFiltering) {
return new OneOfFilter(this.GetClusterKey, valuesChosenForFiltering);
}
/// <summary>
/// Create a label that combines the string representation of the cluster
/// key with a format string that holds an "X [N items in cluster]" type layout.
/// </summary>
/// <param name="cluster"></param>
/// <param name="s"></param>
/// <returns></returns>
virtual protected string ApplyDisplayFormat(ICluster cluster, string s) {
string format = (cluster.Count == 1) ? this.DisplayLabelFormatSingular : this.DisplayLabelFormatPlural;
return String.IsNullOrEmpty(format) ? s : String.Format(format, s, cluster.Count);
}
#endregion
}
}
+70
View File
@@ -0,0 +1,70 @@
/*
* ClusteringStrategy - Implements a simple clustering strategy
*
* Author: Phillip Piper
* Date: 1-April-2011 8:12am
*
* Change log:
* 2011-04-01 JPP - First version
*
* 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 <http://www.gnu.org/licenses/>.
*
* If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com.
*/
using System;
using System.Collections.Generic;
using System.Text;
namespace BrightIdeasSoftware {
/// <summary>
/// This class calculates clusters from the groups that the column uses.
/// </summary>
/// <remarks>
/// <para>
/// This is the default strategy for all non-date, filterable columns.
/// </para>
/// <para>
/// This class does not strictly mimic the groups created by the given column.
/// In particular, if the programmer changes the default grouping technique
/// by listening for grouping events, this class will not mimic that behaviour.
/// </para>
/// </remarks>
public class ClustersFromGroupsStrategy : ClusteringStrategy {
/// <summary>
/// Get the cluster key by which the given model will be partitioned by this strategy
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
public override object GetClusterKey(object model) {
return this.Column.GetGroupKey(model);
}
/// <summary>
/// Gets the display label that the given cluster should use
/// </summary>
/// <param name="cluster"></param>
/// <returns></returns>
public override string GetClusterDisplayLabel(ICluster cluster) {
string s = this.Column.ConvertGroupKeyToTitle(cluster.ClusterKey);
if (String.IsNullOrEmpty(s))
s = EMPTY_LABEL;
return this.ApplyDisplayFormat(cluster, s);
}
}
}
+187
View File
@@ -0,0 +1,187 @@
/*
* DateTimeClusteringStrategy - A strategy to cluster objects by a date time
*
* Author: Phillip Piper
* Date: 30-March-2011 9:40am
*
* Change log:
* 2011-03-30 JPP - First version
*
* 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 <http://www.gnu.org/licenses/>.
*
* If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com.
*/
using System;
using System.Globalization;
namespace BrightIdeasSoftware {
/// <summary>
/// This enum is used to indicate various portions of a datetime
/// </summary>
[Flags]
public enum DateTimePortion {
/// <summary>
/// Year
/// </summary>
Year = 0x01,
/// <summary>
/// Month
/// </summary>
Month = 0x02,
/// <summary>
/// Day of the month
/// </summary>
Day = 0x04,
/// <summary>
/// Hour
/// </summary>
Hour = 0x08,
/// <summary>
/// Minute
/// </summary>
Minute = 0x10,
/// <summary>
/// Second
/// </summary>
Second = 0x20
}
/// <summary>
/// This class implements a strategy where the model objects are clustered
/// according to some portion of the datetime value in the configured column.
/// </summary>
/// <remarks>To create a strategy that grouped people who were born in
/// the same month, you would create a strategy that extracted just
/// the month, and formatted it to show just the month's name. Like this:
/// </remarks>
/// <example>
/// someColumn.ClusteringStrategy = new DateTimeClusteringStrategy(DateTimePortion.Month, "MMMM");
/// </example>
public class DateTimeClusteringStrategy : ClusteringStrategy {
#region Life and death
/// <summary>
/// Create a strategy that clusters by month/year
/// </summary>
public DateTimeClusteringStrategy()
: this(DateTimePortion.Year | DateTimePortion.Month, "MMMM yyyy") {
}
/// <summary>
/// Create a strategy that clusters around the given parts
/// </summary>
/// <param name="portions"></param>
/// <param name="format"></param>
public DateTimeClusteringStrategy(DateTimePortion portions, string format) {
this.Portions = portions;
this.Format = format;
}
#endregion
#region Properties
/// <summary>
/// Gets or sets the format string will will be used to create a user-presentable
/// version of the cluster key.
/// </summary>
/// <remarks>The format should use the date/time format strings, as documented
/// in the Windows SDK. Both standard formats and custom format will work.</remarks>
/// <example>"D" - long date pattern</example>
/// <example>"MMMM, yyyy" - "January, 1999"</example>
public string Format {
get { return format; }
set { format = value; }
}
private string format;
/// <summary>
/// Gets or sets the parts of the DateTime that will be extracted when
/// determining the clustering key for an object.
/// </summary>
public DateTimePortion Portions {
get { return portions; }
set { portions = value; }
}
private DateTimePortion portions = DateTimePortion.Year | DateTimePortion.Month;
#endregion
#region IClusterStrategy implementation
/// <summary>
/// Get the cluster key by which the given model will be partitioned by this strategy
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
public override object GetClusterKey(object model) {
// Get the data attribute we want from the given model
// Make sure the returned value is a DateTime
DateTime? dateTime = this.Column.GetValue(model) as DateTime?;
if (!dateTime.HasValue)
return null;
// Extract the parts of the datetime that we are intereted in.
// Even if we aren't interested in a particular portion, we still have to give it a reasonable default
// otherwise we won't be able to build a DateTime object for it
int year = ((this.Portions & DateTimePortion.Year) == DateTimePortion.Year) ? dateTime.Value.Year : 1;
int month = ((this.Portions & DateTimePortion.Month) == DateTimePortion.Month) ? dateTime.Value.Month : 1;
int day = ((this.Portions & DateTimePortion.Day) == DateTimePortion.Day) ? dateTime.Value.Day : 1;
int hour = ((this.Portions & DateTimePortion.Hour) == DateTimePortion.Hour) ? dateTime.Value.Hour : 0;
int minute = ((this.Portions & DateTimePortion.Minute) == DateTimePortion.Minute) ? dateTime.Value.Minute : 0;
int second = ((this.Portions & DateTimePortion.Second) == DateTimePortion.Second) ? dateTime.Value.Second : 0;
return new DateTime(year, month, day, hour, minute, second);
}
/// <summary>
/// Gets the display label that the given cluster should use
/// </summary>
/// <param name="cluster"></param>
/// <returns></returns>
public override string GetClusterDisplayLabel(ICluster cluster) {
DateTime? dateTime = cluster.ClusterKey as DateTime?;
return this.ApplyDisplayFormat(cluster, dateTime.HasValue ? this.DateToString(dateTime.Value) : NULL_LABEL);
}
/// <summary>
/// Convert the given date into a user presentable string
/// </summary>
/// <param name="dateTime"></param>
/// <returns></returns>
protected virtual string DateToString(DateTime dateTime) {
if (String.IsNullOrEmpty(this.Format))
return dateTime.ToString(CultureInfo.CurrentUICulture);
try {
return dateTime.ToString(this.Format);
}
catch (FormatException) {
return String.Format("Bad format string '{0}' for value '{1}'", this.Format, dateTime);
}
}
#endregion
}
}
+369
View File
@@ -0,0 +1,369 @@
/*
* FilterMenuBuilder - Responsible for creating a Filter menu
*
* Author: Phillip Piper
* Date: 4-March-2011 11:59 pm
*
* Change log:
* 2012-05-20 JPP - Allow the same model object to be in multiple clusters
* Useful for xor'ed flag fields, and multi-value strings
* (e.g. hobbies that are stored as comma separated values).
* v2.5.1
* 2012-04-14 JPP - Fixed rare bug with clustering an empty list (SF #3445118)
* v2.5
* 2011-04-12 JPP - Added some images to menu
* 2011-03-04 JPP - First version
*
* 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 <http://www.gnu.org/licenses/>.
*
* If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com.
*/
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
using System.Collections;
using System.Drawing;
namespace BrightIdeasSoftware {
/// <summary>
/// Instances of this class know how to build a Filter menu.
/// It is responsible for clustering the values in the target column,
/// build a menu that shows those clusters, and then constructing
/// a filter that will enact the users choices.
/// </summary>
/// <remarks>
/// Almost all of the methods in this class are declared as "virtual protected"
/// so that subclasses can provide alternative behaviours.
/// </remarks>
public class FilterMenuBuilder {
#region Static properties
/// <summary>
/// Gets or sets the string that labels the Apply button.
/// Exposed so it can be localized.
/// </summary>
static public string APPLY_LABEL = "Apply";
/// <summary>
/// Gets or sets the string that labels the Clear All menu item.
/// Exposed so it can be localized.
/// </summary>
static public string CLEAR_ALL_FILTERS_LABEL = "Clear All Filters";
/// <summary>
/// Gets or sets the string that labels the Filtering menu as a whole..
/// Exposed so it can be localized.
/// </summary>
static public string FILTERING_LABEL = "Filtering";
/// <summary>
/// Gets or sets the string that represents Select All values.
/// If this is set to null or empty, no Select All option will be included.
/// Exposed so it can be localized.
/// </summary>
static public string SELECT_ALL_LABEL = "Select All";
/// <summary>
/// Gets or sets the image that will be placed next to the Clear Filtering menu item
/// </summary>
static public Bitmap ClearFilteringImage = BrightIdeasSoftware.Properties.Resources.ClearFiltering;
/// <summary>
/// Gets or sets the image that will be placed next to all "Apply" menu items on the filtering menu
/// </summary>
static public Bitmap FilteringImage = BrightIdeasSoftware.Properties.Resources.Filtering;
#endregion
#region Public properties
/// <summary>
/// Gets or sets whether null should be considered as a valid data value.
/// If this is true (the default), then a cluster will null as a key will be allow.
/// If this is false, object that return a cluster key of null will ignored.
/// </summary>
public bool TreatNullAsDataValue {
get { return treatNullAsDataValue; }
set { treatNullAsDataValue = value; }
}
private bool treatNullAsDataValue = true;
/// <summary>
/// Gets or sets the maximum number of objects that the clustering strategy
/// will consider. This should be large enough to collect all unique clusters,
/// but small enough to finish in a reasonable time.
/// </summary>
/// <remarks>The default value is 10,000. This should be perfectly
/// acceptable for almost all lists.</remarks>
public int MaxObjectsToConsider {
get { return maxObjectsToConsider; }
set { maxObjectsToConsider = value; }
}
private int maxObjectsToConsider = 10000;
#endregion
/// <summary>
/// Create a Filter menu on the given tool tip for the given column in the given ObjectListView.
/// </summary>
/// <remarks>This is the main entry point into this class.</remarks>
/// <param name="strip"></param>
/// <param name="listView"></param>
/// <param name="column"></param>
/// <returns>The strip that should be shown to the user</returns>
virtual public ToolStripDropDown MakeFilterMenu(ToolStripDropDown strip, ObjectListView listView, OLVColumn column) {
if (strip == null) throw new ArgumentNullException("strip");
if (listView == null) throw new ArgumentNullException("listView");
if (column == null) throw new ArgumentNullException("column");
if (!column.UseFiltering || column.ClusteringStrategy == null || listView.Objects == null)
return strip;
List<ICluster> clusters = this.Cluster(column.ClusteringStrategy, listView, column);
if (clusters.Count > 0) {
this.SortClusters(column.ClusteringStrategy, clusters);
strip.Items.Add(this.CreateFilteringMenuItem(column, clusters));
}
return strip;
}
/// <summary>
/// Create a collection of clusters that should be presented to the user
/// </summary>
/// <param name="strategy"></param>
/// <param name="listView"></param>
/// <param name="column"></param>
/// <returns></returns>
virtual protected List<ICluster> Cluster(IClusteringStrategy strategy, ObjectListView listView, OLVColumn column) {
// Build a map that correlates cluster key to clusters
NullableDictionary<object, ICluster> map = new NullableDictionary<object, ICluster>();
int count = 0;
foreach (object model in listView.ObjectsForClustering) {
this.ClusterOneModel(strategy, map, model);
if (count++ > this.MaxObjectsToConsider)
break;
}
// Now that we know exactly how many items are in each cluster, create a label for it
foreach (ICluster cluster in map.Values)
cluster.DisplayLabel = strategy.GetClusterDisplayLabel(cluster);
return new List<ICluster>(map.Values);
}
private void ClusterOneModel(IClusteringStrategy strategy, NullableDictionary<object, ICluster> map, object model) {
object clusterKey = strategy.GetClusterKey(model);
// If the returned value is an IEnumerable, that means the given model can belong to more than one cluster
IEnumerable keyEnumerable = clusterKey as IEnumerable;
if (clusterKey is string || keyEnumerable == null)
keyEnumerable = new object[] {clusterKey};
// Deal with nulls and DBNulls
ArrayList nullCorrected = new ArrayList();
foreach (object key in keyEnumerable) {
if (key == null || key == System.DBNull.Value) {
if (this.TreatNullAsDataValue)
nullCorrected.Add(null);
} else nullCorrected.Add(key);
}
// Group by key
foreach (object key in nullCorrected) {
if (map.ContainsKey(key))
map[key].Count += 1;
else
map[key] = strategy.CreateCluster(key);
}
}
/// <summary>
/// Order the given list of clusters in the manner in which they should be presented to the user.
/// </summary>
/// <param name="strategy"></param>
/// <param name="clusters"></param>
virtual protected void SortClusters(IClusteringStrategy strategy, List<ICluster> clusters) {
clusters.Sort();
}
/// <summary>
/// Do the work of making a menu that shows the clusters to the users
/// </summary>
/// <param name="column"></param>
/// <param name="clusters"></param>
/// <returns></returns>
virtual protected ToolStripMenuItem CreateFilteringMenuItem(OLVColumn column, List<ICluster> clusters) {
ToolStripCheckedListBox checkedList = new ToolStripCheckedListBox();
checkedList.Tag = column;
foreach (ICluster cluster in clusters)
checkedList.AddItem(cluster, column.ValuesChosenForFiltering.Contains(cluster.ClusterKey));
if (!String.IsNullOrEmpty(SELECT_ALL_LABEL)) {
int checkedCount = checkedList.CheckedItems.Count;
if (checkedCount == 0)
checkedList.AddItem(SELECT_ALL_LABEL, CheckState.Unchecked);
else
checkedList.AddItem(SELECT_ALL_LABEL, checkedCount == clusters.Count ? CheckState.Checked : CheckState.Indeterminate);
}
checkedList.ItemCheck += new ItemCheckEventHandler(HandleItemCheckedWrapped);
ToolStripMenuItem clearAll = new ToolStripMenuItem(CLEAR_ALL_FILTERS_LABEL, ClearFilteringImage, delegate(object sender, EventArgs args) {
this.ClearAllFilters(column);
});
ToolStripMenuItem apply = new ToolStripMenuItem(APPLY_LABEL, FilteringImage, delegate(object sender, EventArgs args) {
this.EnactFilter(checkedList, column);
});
ToolStripMenuItem subMenu = new ToolStripMenuItem(FILTERING_LABEL, null, new ToolStripItem[] {
clearAll, new ToolStripSeparator(), checkedList, apply });
return subMenu;
}
/// <summary>
/// Wrap a protected section around the real HandleItemChecked method, so that if
/// that method tries to change a "checkedness" of an item, we don't get a recursive
/// stack error. Effectively, this ensure that HandleItemChecked is only called
/// in response to a user action.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void HandleItemCheckedWrapped(object sender, ItemCheckEventArgs e) {
if (alreadyInHandleItemChecked)
return;
try {
alreadyInHandleItemChecked = true;
this.HandleItemChecked(sender, e);
}
finally {
alreadyInHandleItemChecked = false;
}
}
bool alreadyInHandleItemChecked = false;
/// <summary>
/// Handle a user-generated ItemCheck event
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
virtual protected void HandleItemChecked(object sender, ItemCheckEventArgs e) {
ToolStripCheckedListBox checkedList = sender as ToolStripCheckedListBox;
if (checkedList == null) return;
OLVColumn column = checkedList.Tag as OLVColumn;
if (column == null) return;
ObjectListView listView = column.ListView as ObjectListView;
if (listView == null) return;
// Deal with the "Select All" item if there is one
int selectAllIndex = checkedList.Items.IndexOf(SELECT_ALL_LABEL);
if (selectAllIndex >= 0)
HandleSelectAllItem(e, checkedList, selectAllIndex);
}
/// <summary>
/// Handle any checking/unchecking of the Select All option, and keep
/// its checkedness in sync with everything else that is checked.
/// </summary>
/// <param name="e"></param>
/// <param name="checkedList"></param>
/// <param name="selectAllIndex"></param>
virtual protected void HandleSelectAllItem(ItemCheckEventArgs e, ToolStripCheckedListBox checkedList, int selectAllIndex) {
// Did they check/uncheck the "Select All"?
if (e.Index == selectAllIndex) {
if (e.NewValue == CheckState.Checked)
checkedList.CheckAll();
if (e.NewValue == CheckState.Unchecked)
checkedList.UncheckAll();
return;
}
// OK. The user didn't check/uncheck SelectAll. Now we have to update it's
// checkedness to reflect the state of everything else
// If all clusters are checked, we check the Select All.
// If no clusters are checked, the uncheck the Select All.
// For everything else, Select All is set to indeterminate.
// How many items are currenty checked?
int count = checkedList.CheckedItems.Count;
// First complication.
// The value of the Select All itself doesn't count
if (checkedList.GetItemCheckState(selectAllIndex) != CheckState.Unchecked)
count -= 1;
// Another complication.
// CheckedItems does not yet know about the item the user has just
// clicked, so we have to adjust the count of checked items to what
// it is going to be
if (e.NewValue != e.CurrentValue) {
if (e.NewValue == CheckState.Checked)
count += 1;
else
count -= 1;
}
// Update the state of the Select All item
if (count == 0)
checkedList.SetItemState(selectAllIndex, CheckState.Unchecked);
else if (count == checkedList.Items.Count - 1)
checkedList.SetItemState(selectAllIndex, CheckState.Checked);
else
checkedList.SetItemState(selectAllIndex, CheckState.Indeterminate);
}
/// <summary>
/// Clear all the filters that are applied to the given column
/// </summary>
/// <param name="column">The column from which filters are to be removed</param>
virtual protected void ClearAllFilters(OLVColumn column) {
ObjectListView olv = column.ListView as ObjectListView;
if (olv == null || olv.IsDisposed)
return;
olv.ResetColumnFiltering();
}
/// <summary>
/// Apply the selected values from the given list as a filter on the given column
/// </summary>
/// <param name="checkedList">A list in which the checked items should be used as filters</param>
/// <param name="column">The column for which a filter should be generated</param>
virtual protected void EnactFilter(ToolStripCheckedListBox checkedList, OLVColumn column) {
ObjectListView olv = column.ListView as ObjectListView;
if (olv == null || olv.IsDisposed)
return;
// Collect all the checked values
ArrayList chosenValues = new ArrayList();
foreach (object x in checkedList.CheckedItems) {
ICluster cluster = x as ICluster;
if (cluster != null) {
chosenValues.Add(cluster.ClusterKey);
}
}
column.ValuesChosenForFiltering = chosenValues;
olv.UpdateColumnFiltering();
}
}
}
+476
View File
@@ -0,0 +1,476 @@
/*
* Filters - Filtering on ObjectListViews
*
* Author: Phillip Piper
* Date: 03/03/2010 17:00
*
* Change log:
* 2011-03-01 JPP Added CompositeAllFilter, CompositeAnyFilter and OneOfFilter
* v2.4.1
* 2010-06-23 JPP Extended TextMatchFilter to handle regular expressions and string prefix matching.
* v2.4
* 2010-03-03 JPP Initial version
*
* TO DO:
*
* Copyright (C) 2010-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 <http://www.gnu.org/licenses/>.
*
* If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com.
*/
using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Reflection;
using System.Drawing;
namespace BrightIdeasSoftware
{
/// <summary>
/// Interface for model-by-model filtering
/// </summary>
public interface IModelFilter
{
/// <summary>
/// Should the given model be included when this filter is installed
/// </summary>
/// <param name="modelObject">The model object to consider</param>
/// <returns>Returns true if the model will be included by the filter</returns>
bool Filter(object modelObject);
}
/// <summary>
/// Interface for whole list filtering
/// </summary>
public interface IListFilter
{
/// <summary>
/// Return a subset of the given list of model objects as the new
/// contents of the ObjectListView
/// </summary>
/// <param name="modelObjects">The collection of model objects that the list will possibly display</param>
/// <returns>The filtered collection that holds the model objects that will be displayed.</returns>
IEnumerable Filter(IEnumerable modelObjects);
}
/// <summary>
/// Base class for model-by-model filters
/// </summary>
public class AbstractModelFilter : IModelFilter
{
/// <summary>
/// Should the given model be included when this filter is installed
/// </summary>
/// <param name="modelObject">The model object to consider</param>
/// <returns>Returns true if the model will be included by the filter</returns>
virtual public bool Filter(object modelObject) {
return true;
}
}
/// <summary>
/// This filter calls a given Predicate to decide if a model object should be included
/// </summary>
public class ModelFilter : IModelFilter
{
/// <summary>
/// Create a filter based on the given predicate
/// </summary>
/// <param name="predicate">The function that will filter objects</param>
public ModelFilter(Predicate<object> predicate) {
this.Predicate = predicate;
}
/// <summary>
/// Gets or sets the predicate used to filter model objects
/// </summary>
protected Predicate<object> Predicate {
get { return predicate; }
set { predicate = value; }
}
private Predicate<object> predicate;
/// <summary>
/// Should the given model object be included?
/// </summary>
/// <param name="modelObject"></param>
/// <returns></returns>
virtual public bool Filter(object modelObject) {
return this.Predicate == null ? true : this.Predicate(modelObject);
}
}
/// <summary>
/// A CompositeFilter joins several other filters together.
/// If there are no filters, all model objects are included
/// </summary>
abstract public class CompositeFilter : IModelFilter {
/// <summary>
/// Create an empty filter
/// </summary>
public CompositeFilter() {
}
/// <summary>
/// Create a composite filter from the given list of filters
/// </summary>
/// <param name="filters">A list of filters</param>
public CompositeFilter(IEnumerable<IModelFilter> filters) {
foreach (IModelFilter filter in filters) {
if (filter != null)
Filters.Add(filter);
}
}
/// <summary>
/// Gets or sets the filters used by this composite
/// </summary>
public IList<IModelFilter> Filters {
get { return filters; }
set { filters = value; }
}
private IList<IModelFilter> filters = new List<IModelFilter>();
/// <summary>
/// Decide whether or not the given model should be included by the filter
/// </summary>
/// <param name="modelObject"></param>
/// <returns>True if the object is included by the filter</returns>
virtual public bool Filter(object modelObject) {
if (this.Filters == null || this.Filters.Count == 0)
return true;
return this.FilterObject(modelObject);
}
/// <summary>
/// Decide whether or not the given model should be included by the filter
/// </summary>
/// <remarks>Filters is guaranteed to be non-empty when this method is called</remarks>
/// <param name="modelObject">The model object under consideration</param>
/// <returns>True if the object is included by the filter</returns>
abstract public bool FilterObject(object modelObject);
}
/// <summary>
/// A CompositeAllFilter joins several other filters together.
/// A model object must satisfy all filters to be included.
/// If there are no filters, all model objects are included
/// </summary>
public class CompositeAllFilter : CompositeFilter {
/// <summary>
/// Create a filter
/// </summary>
/// <param name="filters"></param>
public CompositeAllFilter(List<IModelFilter> filters)
: base(filters) {
}
/// <summary>
/// Decide whether or not the given model should be included by the filter
/// </summary>
/// <remarks>Filters is guaranteed to be non-empty when this method is called</remarks>
/// <param name="modelObject">The model object under consideration</param>
/// <returns>True if the object is included by the filter</returns>
override public bool FilterObject(object modelObject) {
foreach (IModelFilter filter in this.Filters)
if (!filter.Filter(modelObject))
return false;
return true;
}
}
/// <summary>
/// A CompositeAllFilter joins several other filters together.
/// A model object must only satisfy one of the filters to be included.
/// If there are no filters, all model objects are included
/// </summary>
public class CompositeAnyFilter : CompositeFilter {
/// <summary>
/// Create a filter from the given filters
/// </summary>
/// <param name="filters"></param>
public CompositeAnyFilter(List<IModelFilter> filters)
: base(filters) {
}
/// <summary>
/// Decide whether or not the given model should be included by the filter
/// </summary>
/// <remarks>Filters is guaranteed to be non-empty when this method is called</remarks>
/// <param name="modelObject">The model object under consideration</param>
/// <returns>True if the object is included by the filter</returns>
override public bool FilterObject(object modelObject) {
foreach (IModelFilter filter in this.Filters)
if (filter.Filter(modelObject))
return true;
return false;
}
}
/// <summary>
/// Instances of this class extract a value from the model object
/// and compare that value to a list of fixed values. The model
/// object is included if the extracted value is in the list
/// </summary>
/// <remarks>If there is no delegate installed or there are
/// no values to match, no model objects will be matched</remarks>
public class OneOfFilter : IModelFilter {
/// <summary>
/// Create a filter that will use the given delegate to extract values
/// </summary>
/// <param name="valueGetter"></param>
public OneOfFilter(AspectGetterDelegate valueGetter) :
this(valueGetter, new ArrayList()) {
}
/// <summary>
/// Create a filter that will extract values using the given delegate
/// and compare them to the values in the given list.
/// </summary>
/// <param name="valueGetter"></param>
/// <param name="possibleValues"></param>
public OneOfFilter(AspectGetterDelegate valueGetter, ICollection possibleValues) {
this.ValueGetter = valueGetter;
this.PossibleValues = new ArrayList(possibleValues);
}
/// <summary>
/// Gets or sets the delegate that will be used to extract values
/// from model objects
/// </summary>
virtual public AspectGetterDelegate ValueGetter {
get { return valueGetter; }
set { valueGetter = value; }
}
private AspectGetterDelegate valueGetter;
/// <summary>
/// Gets or sets the list of values that the value extracted from
/// the model object must match in order to be included.
/// </summary>
virtual public IList PossibleValues {
get { return possibleValues; }
set { possibleValues = value; }
}
private IList possibleValues;
/// <summary>
/// Should the given model object be included?
/// </summary>
/// <param name="modelObject"></param>
/// <returns></returns>
public virtual bool Filter(object modelObject) {
if (this.ValueGetter == null || this.PossibleValues == null || this.PossibleValues.Count == 0)
return false;
object result = this.ValueGetter(modelObject);
IEnumerable enumerable = result as IEnumerable;
if (result is string || enumerable == null)
return this.DoesValueMatch(result);
foreach (object x in enumerable) {
if (this.DoesValueMatch(x))
return true;
}
return false;
}
/// <summary>
/// Decides if the given property is a match for the values in the PossibleValues collection
/// </summary>
/// <param name="result"></param>
/// <returns></returns>
protected virtual bool DoesValueMatch(object result) {
return this.PossibleValues.Contains(result);
}
}
/// <summary>
/// Instances of this class match a property of a model objects against
/// a list of bit flags. The property should be an xor-ed collection
/// of bits flags.
/// </summary>
/// <remarks>Both the property compared and the list of possible values
/// must be convertible to ulongs.</remarks>
public class FlagBitSetFilter : OneOfFilter {
/// <summary>
/// Create an instance
/// </summary>
/// <param name="valueGetter"></param>
/// <param name="possibleValues"></param>
public FlagBitSetFilter(AspectGetterDelegate valueGetter, ICollection possibleValues) : base(valueGetter, possibleValues) {
this.ConvertPossibleValues();
}
/// <summary>
/// Gets or sets the collection of values that will be matched.
/// These must be ulongs (or convertible to ulongs).
/// </summary>
public override IList PossibleValues {
get { return base.PossibleValues; }
set {
base.PossibleValues = value;
this.ConvertPossibleValues();
}
}
private void ConvertPossibleValues() {
this.possibleValuesAsUlongs = new List<UInt64>();
foreach (object x in this.PossibleValues)
this.possibleValuesAsUlongs.Add(Convert.ToUInt64(x));
}
/// <summary>
/// Decides if the given property is a match for the values in the PossibleValues collection
/// </summary>
/// <param name="result"></param>
/// <returns></returns>
protected override bool DoesValueMatch(object result) {
try {
UInt64 value = Convert.ToUInt64(result);
foreach (ulong flag in this.possibleValuesAsUlongs) {
if ((value & flag) == flag)
return true;
}
return false;
}
catch (InvalidCastException) {
return false;
}
catch (FormatException) {
return false;
}
}
private List<UInt64> possibleValuesAsUlongs = new List<UInt64>();
}
/// <summary>
/// Base class for whole list filters
/// </summary>
public class AbstractListFilter : IListFilter
{
/// <summary>
/// Return a subset of the given list of model objects as the new
/// contents of the ObjectListView
/// </summary>
/// <param name="modelObjects">The collection of model objects that the list will possibly display</param>
/// <returns>The filtered collection that holds the model objects that will be displayed.</returns>
virtual public IEnumerable Filter(IEnumerable modelObjects) {
return modelObjects;
}
}
/// <summary>
/// Instance of this class implement delegate based whole list filtering
/// </summary>
public class ListFilter : AbstractListFilter
{
/// <summary>
/// A delegate that filters on a whole list
/// </summary>
/// <param name="rowObjects"></param>
/// <returns></returns>
public delegate IEnumerable ListFilterDelegate(IEnumerable rowObjects);
/// <summary>
/// Create a ListFilter
/// </summary>
/// <param name="function"></param>
public ListFilter(ListFilterDelegate function) {
this.Function = function;
}
/// <summary>
/// Gets or sets the delegate that will filter the list
/// </summary>
public ListFilterDelegate Function {
get { return function; }
set { function = value; }
}
private ListFilterDelegate function;
/// <summary>
/// Do the actual work of filtering
/// </summary>
/// <param name="modelObjects"></param>
/// <returns></returns>
public override IEnumerable Filter(IEnumerable modelObjects) {
if (this.Function == null)
return modelObjects;
return this.Function(modelObjects);
}
}
/// <summary>
/// Filter the list so only the last N entries are displayed
/// </summary>
public class TailFilter : AbstractListFilter
{
/// <summary>
/// Create a no-op tail filter
/// </summary>
public TailFilter() {
}
/// <summary>
/// Create a filter that includes on the last N model objects
/// </summary>
/// <param name="numberOfObjects"></param>
public TailFilter(int numberOfObjects) {
this.Count = numberOfObjects;
}
/// <summary>
/// Gets or sets the number of model objects that will be
/// returned from the tail of the list
/// </summary>
public int Count {
get { return count; }
set { count = value; }
}
private int count;
/// <summary>
/// Return the last N subset of the model objects
/// </summary>
/// <param name="modelObjects"></param>
/// <returns></returns>
public override IEnumerable Filter(IEnumerable modelObjects) {
if (this.Count <= 0)
return modelObjects;
ArrayList list = ObjectListView.EnumerableToArray(modelObjects, false);
if (this.Count > list.Count)
return list;
object[] tail = new object[this.Count];
list.CopyTo(list.Count - this.Count, tail, 0, this.Count);
return new ArrayList(tail);
}
}
}
+160
View File
@@ -0,0 +1,160 @@
/*
* FlagClusteringStrategy - Implements a clustering strategy for a field which is a single integer
* containing an XOR'ed collection of bit flags
*
* Author: Phillip Piper
* Date: 23-March-2012 8:33 am
*
* Change log:
* 2012-03-23 JPP - First version
*
* Copyright (C) 2012 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 <http://www.gnu.org/licenses/>.
*
* If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com.
*/
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
namespace BrightIdeasSoftware {
/// <summary>
/// Instances of this class cluster model objects on the basis of a
/// property that holds an xor-ed collection of bit flags.
/// </summary>
public class FlagClusteringStrategy : ClusteringStrategy
{
#region Life and death
/// <summary>
/// Create a clustering strategy that operates on the flags of the given enum
/// </summary>
/// <param name="enumType"></param>
public FlagClusteringStrategy(Type enumType) {
if (enumType == null) throw new ArgumentNullException("enumType");
if (!enumType.IsEnum) throw new ArgumentException("Type must be enum", "enumType");
if (enumType.GetCustomAttributes(typeof(FlagsAttribute), false) == null) throw new ArgumentException("Type must have [Flags] attribute", "enumType");
List<long> flags = new List<long>();
foreach (object x in Enum.GetValues(enumType))
flags.Add(Convert.ToInt64(x));
List<string> flagLabels = new List<string>();
foreach (string x in Enum.GetNames(enumType))
flagLabels.Add(x);
this.SetValues(flags.ToArray(), flagLabels.ToArray());
}
/// <summary>
/// Create a clustering strategy around the given collections of flags and their display labels.
/// There must be the same number of elements in both collections.
/// </summary>
/// <param name="values">The list of flags. </param>
/// <param name="labels"></param>
public FlagClusteringStrategy(long[] values, string[] labels) {
this.SetValues(values, labels);
}
#endregion
#region Implementation
/// <summary>
/// Gets the value that will be xor-ed to test for the presence of a particular value.
/// </summary>
public long[] Values {
get { return this.values; }
private set { this.values = value; }
}
private long[] values;
/// <summary>
/// Gets the labels that will be used when the corresponding Value is XOR present in the data.
/// </summary>
public string[] Labels {
get { return this.labels; }
private set { this.labels = value; }
}
private string[] labels;
private void SetValues(long[] flags, string[] flagLabels) {
if (flags == null || flags.Length == 0) throw new ArgumentNullException("flags");
if (flagLabels == null || flagLabels.Length == 0) throw new ArgumentNullException("flagLabels");
if (flags.Length != flagLabels.Length) throw new ArgumentException("values and labels must have the same number of entries", "flags");
this.Values = flags;
this.Labels = flagLabels;
}
#endregion
#region Implementation of IClusteringStrategy
/// <summary>
/// Get the cluster key by which the given model will be partitioned by this strategy
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
public override object GetClusterKey(object model) {
List<long> flags = new List<long>();
try {
long modelValue = Convert.ToInt64(this.Column.GetValue(model));
foreach (long x in this.Values) {
if ((x & modelValue) == x)
flags.Add(x);
}
return flags;
}
catch (InvalidCastException ex) {
System.Diagnostics.Debug.Write(ex);
return flags;
}
catch (FormatException ex) {
System.Diagnostics.Debug.Write(ex);
return flags;
}
}
/// <summary>
/// Gets the display label that the given cluster should use
/// </summary>
/// <param name="cluster"></param>
/// <returns></returns>
public override string GetClusterDisplayLabel(ICluster cluster) {
long clusterKeyAsUlong = Convert.ToInt64(cluster.ClusterKey);
for (int i = 0; i < this.Values.Length; i++ ) {
if (clusterKeyAsUlong == this.Values[i])
return this.ApplyDisplayFormat(cluster, this.Labels[i]);
}
return this.ApplyDisplayFormat(cluster, clusterKeyAsUlong.ToString(CultureInfo.CurrentUICulture));
}
/// <summary>
/// Create a filter that will include only model objects that
/// match one or more of the given values.
/// </summary>
/// <param name="valuesChosenForFiltering"></param>
/// <returns></returns>
public override IModelFilter CreateFilter(IList valuesChosenForFiltering) {
return new FlagBitSetFilter(this.GetClusterKey, valuesChosenForFiltering);
}
#endregion
}
}
+56
View File
@@ -0,0 +1,56 @@
/*
* ICluster - A cluster is a group of objects that can be included or excluded as a whole
*
* Author: Phillip Piper
* Date: 4-March-2011 11:59 pm
*
* Change log:
* 2011-03-04 JPP - First version
*
* 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 <http://www.gnu.org/licenses/>.
*
* If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com.
*/
using System;
using System.Collections.Generic;
using System.Text;
namespace BrightIdeasSoftware {
/// <summary>
/// A cluster is a like collection of objects that can be usefully filtered
/// as whole using the filtering UI provided by the ObjectListView.
/// </summary>
public interface ICluster : IComparable {
/// <summary>
/// Gets or sets how many items belong to this cluster
/// </summary>
int Count { get; set; }
/// <summary>
/// Gets or sets the label that will be shown to the user to represent
/// this cluster
/// </summary>
string DisplayLabel { get; set; }
/// <summary>
/// Gets or sets the actual data object that all members of this cluster
/// have commonly returned.
/// </summary>
object ClusterKey { get; set; }
}
}
+80
View File
@@ -0,0 +1,80 @@
/*
* IClusterStrategy - Encapsulates the ability to create a list of clusters from an ObjectListView
*
* Author: Phillip Piper
* Date: 4-March-2011 11:59 pm
*
* Change log:
* 2012-05-23 JPP - Added CreateFilter() method to interface to allow the strategy
* to control the actual model filter that is created.
* v2.5
* 2011-03-04 JPP - First version
*
* 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 <http://www.gnu.org/licenses/>.
*
* If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com.
*/
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
namespace BrightIdeasSoftware{
/// <summary>
/// Implementation of this interface control the selecting of cluster keys
/// and how those clusters will be presented to the user
/// </summary>
public interface IClusteringStrategy {
/// <summary>
/// Gets or sets the column upon which this strategy will operate
/// </summary>
OLVColumn Column { get; set; }
/// <summary>
/// Get the cluster key by which the given model will be partitioned by this strategy
/// </summary>
/// <remarks>If the returned value is an IEnumerable, the given model is considered
/// to belong to multiple clusters</remarks>
/// <param name="model"></param>
/// <returns></returns>
object GetClusterKey(object model);
/// <summary>
/// Create a cluster to hold the given cluster key
/// </summary>
/// <param name="clusterKey"></param>
/// <returns></returns>
ICluster CreateCluster(object clusterKey);
/// <summary>
/// Gets the display label that the given cluster should use
/// </summary>
/// <param name="cluster"></param>
/// <returns></returns>
string GetClusterDisplayLabel(ICluster cluster);
/// <summary>
/// Create a filter that will include only model objects that
/// match one or more of the given values.
/// </summary>
/// <param name="valuesChosenForFiltering"></param>
/// <returns></returns>
IModelFilter CreateFilter(IList valuesChosenForFiltering);
}
}
+623
View File
@@ -0,0 +1,623 @@
/*
* TextMatchFilter - Text based filtering on ObjectListViews
*
* Author: Phillip Piper
* Date: 31/05/2011 7:45am
*
* Change log:
* v2.6
* 2012-10-13 JPP Allow filtering to consider additional columns
* v2.5.1
* 2011-06-22 JPP Handle searching for empty strings
* v2.5.0
* 2011-05-31 JPP Initial version
*
* TO DO:
*
* 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 <http://www.gnu.org/licenses/>.
*
* If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com.
*/
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using System.Drawing;
namespace BrightIdeasSoftware {
/// <summary>
/// Instances of this class include only those rows of the listview
/// that match one or more given strings.
/// </summary>
/// <remarks>This class can match strings by prefix, regex, or simple containment.
/// There are factory methods for each of these matching strategies.</remarks>
public class TextMatchFilter : AbstractModelFilter {
#region Life and death
/// <summary>
/// Create a text filter that will include rows where any cell matches
/// any of the given regex expressions.
/// </summary>
/// <param name="olv"></param>
/// <param name="texts"></param>
/// <returns></returns>
/// <remarks>Any string that is not a valid regex expression will be ignored.</remarks>
public static TextMatchFilter Regex(ObjectListView olv, params string[] texts) {
TextMatchFilter filter = new TextMatchFilter(olv);
filter.RegexStrings = texts;
return filter;
}
/// <summary>
/// Create a text filter that includes rows where any cell begins with one of the given strings
/// </summary>
/// <param name="olv"></param>
/// <param name="texts"></param>
/// <returns></returns>
public static TextMatchFilter Prefix(ObjectListView olv, params string[] texts) {
TextMatchFilter filter = new TextMatchFilter(olv);
filter.PrefixStrings = texts;
return filter;
}
/// <summary>
/// Create a text filter that includes rows where any cell contains any of the given strings.
/// </summary>
/// <param name="olv"></param>
/// <param name="texts"></param>
/// <returns></returns>
public static TextMatchFilter Contains(ObjectListView olv, params string[] texts) {
TextMatchFilter filter = new TextMatchFilter(olv);
filter.ContainsStrings = texts;
return filter;
}
/// <summary>
/// Create a TextFilter
/// </summary>
/// <param name="olv"></param>
public TextMatchFilter(ObjectListView olv) {
this.ListView = olv;
}
/// <summary>
/// Create a TextFilter that finds the given string
/// </summary>
/// <param name="olv"></param>
/// <param name="text"></param>
public TextMatchFilter(ObjectListView olv, string text) {
this.ListView = olv;
this.ContainsStrings = new string[] { text };
}
/// <summary>
/// Create a TextFilter that finds the given string using the given comparison
/// </summary>
/// <param name="olv"></param>
/// <param name="text"></param>
/// <param name="comparison"></param>
public TextMatchFilter(ObjectListView olv, string text, StringComparison comparison) {
this.ListView = olv;
this.ContainsStrings = new string[] { text };
this.StringComparison = comparison;
}
#endregion
#region Public properties
/// <summary>
/// Gets or sets which columns will be used for the comparisons? If this is null, all columns will be used
/// </summary>
public OLVColumn[] Columns {
get { return columns; }
set { columns = value; }
}
private OLVColumn[] columns;
/// <summary>
/// Gets or sets additional columns which will be used in the comparison. These will be used
/// in addition to either the Columns property or to all columns taken from the control.
/// </summary>
public OLVColumn[] AdditionalColumns {
get { return additionalColumns; }
set { additionalColumns = value; }
}
private OLVColumn[] additionalColumns;
/// <summary>
/// Gets or sets the collection of strings that will be used for
/// contains matching. Setting this replaces all previous texts
/// of any kind.
/// </summary>
public IEnumerable<string> ContainsStrings {
get {
foreach (TextMatchingStrategy component in this.MatchingStrategies)
yield return component.Text;
}
set {
this.MatchingStrategies = new List<TextMatchingStrategy>();
if (value != null) {
foreach (string text in value)
this.MatchingStrategies.Add(new TextContainsMatchingStrategy(this, text));
}
}
}
/// <summary>
/// Gets whether or not this filter has any search criteria
/// </summary>
public bool HasComponents {
get {
return this.MatchingStrategies.Count > 0;
}
}
/// <summary>
/// Gets or set the ObjectListView upon which this filter will work
/// </summary>
/// <remarks>
/// You cannot really rebase a filter after it is created, so do not change this value.
/// It is included so that it can be set in an object initializer.
/// </remarks>
public ObjectListView ListView {
get { return listView; }
set { listView = value; }
}
private ObjectListView listView;
/// <summary>
/// Gets or sets the collection of strings that will be used for
/// prefix matching. Setting this replaces all previous texts
/// of any kind.
/// </summary>
public IEnumerable<string> PrefixStrings {
get {
foreach (TextMatchingStrategy component in this.MatchingStrategies)
yield return component.Text;
}
set {
this.MatchingStrategies = new List<TextMatchingStrategy>();
if (value != null) {
foreach (string text in value)
this.MatchingStrategies.Add(new TextBeginsMatchingStrategy(this, text));
}
}
}
/// <summary>
/// Gets or sets the options that will be used when compiling the regular expression.
/// </summary>
/// <remarks>
/// This is only used when doing Regex matching (obviously).
/// If this is not set specifically, the appropriate options are chosen to match the
/// StringComparison setting (culture invariant, case sensitive).
/// </remarks>
public RegexOptions RegexOptions {
get {
if (!regexOptions.HasValue) {
switch (this.StringComparison) {
case StringComparison.CurrentCulture:
regexOptions = RegexOptions.None;
break;
case StringComparison.CurrentCultureIgnoreCase:
regexOptions = RegexOptions.IgnoreCase;
break;
case StringComparison.Ordinal:
case StringComparison.InvariantCulture:
regexOptions = RegexOptions.CultureInvariant;
break;
case StringComparison.OrdinalIgnoreCase:
case StringComparison.InvariantCultureIgnoreCase:
regexOptions = RegexOptions.CultureInvariant | RegexOptions.IgnoreCase;
break;
default:
regexOptions = RegexOptions.None;
break;
}
}
return regexOptions.Value;
}
set {
regexOptions = value;
}
}
private RegexOptions? regexOptions;
/// <summary>
/// Gets or sets the collection of strings that will be used for
/// regex pattern matching. Setting this replaces all previous texts
/// of any kind.
/// </summary>
public IEnumerable<string> RegexStrings {
get {
foreach (TextMatchingStrategy component in this.MatchingStrategies)
yield return component.Text;
}
set {
this.MatchingStrategies = new List<TextMatchingStrategy>();
if (value != null) {
foreach (string text in value)
this.MatchingStrategies.Add(new TextRegexMatchingStrategy(this, text));
}
}
}
/// <summary>
/// Gets or sets how the filter will match text
/// </summary>
public StringComparison StringComparison {
get { return this.stringComparison; }
set { this.stringComparison = value; }
}
private StringComparison stringComparison = StringComparison.InvariantCultureIgnoreCase;
#endregion
#region Implementation
/// <summary>
/// Loop over the columns that are being considering by the filter
/// </summary>
/// <returns></returns>
protected virtual IEnumerable<OLVColumn> IterateColumns() {
if (this.Columns == null) {
foreach (OLVColumn column in this.ListView.Columns)
yield return column;
} else {
foreach (OLVColumn column in this.Columns)
yield return column;
}
if (this.AdditionalColumns != null) {
foreach (OLVColumn column in this.AdditionalColumns)
yield return column;
}
}
#endregion
#region Public interface
/// <summary>
/// Do the actual work of filtering
/// </summary>
/// <param name="modelObject"></param>
/// <returns></returns>
public override bool Filter(object modelObject) {
if (this.ListView == null || !this.HasComponents)
return true;
foreach (OLVColumn column in this.IterateColumns()) {
if (column.IsVisible && column.Searchable) {
string cellText = column.GetStringValue(modelObject);
foreach (TextMatchingStrategy filter in this.MatchingStrategies) {
if (String.IsNullOrEmpty(filter.Text) || filter.MatchesText(cellText))
return true;
}
}
}
return false;
}
/// <summary>
/// Find all the ways in which this filter matches the given string.
/// </summary>
/// <remarks>This is used by the renderer to decide which bits of
/// the string should be highlighted</remarks>
/// <param name="cellText"></param>
/// <returns>A list of character ranges indicating the matched substrings</returns>
public IEnumerable<CharacterRange> FindAllMatchedRanges(string cellText) {
List<CharacterRange> ranges = new List<CharacterRange>();
foreach (TextMatchingStrategy filter in this.MatchingStrategies) {
if (!String.IsNullOrEmpty(filter.Text))
ranges.AddRange(filter.FindAllMatchedRanges(cellText));
}
return ranges;
}
/// <summary>
/// Is the given column one of the columns being used by this filter?
/// </summary>
/// <param name="column"></param>
/// <returns></returns>
public bool IsIncluded(OLVColumn column) {
if (this.Columns == null) {
return column.ListView == this.ListView;
}
foreach (OLVColumn x in this.Columns) {
if (x == column)
return true;
}
return false;
}
#endregion
#region Implementation members
private List<TextMatchingStrategy> MatchingStrategies = new List<TextMatchingStrategy>();
#endregion
#region Components
/// <summary>
/// Base class for the various types of string matching that TextMatchFilter provides
/// </summary>
abstract protected class TextMatchingStrategy {
/// <summary>
/// Gets how the filter will match text
/// </summary>
public StringComparison StringComparison {
get { return this.TextFilter.StringComparison; }
}
/// <summary>
/// Gets the text filter to which this component belongs
/// </summary>
public TextMatchFilter TextFilter {
get { return textFilter; }
set { textFilter = value; }
}
private TextMatchFilter textFilter;
/// <summary>
/// Gets or sets the text that will be matched
/// </summary>
public string Text {
get { return this.text; }
set { this.text = value; }
}
private string text;
/// <summary>
/// Find all the ways in which this filter matches the given string.
/// </summary>
/// <remarks>
/// <para>
/// This is used by the renderer to decide which bits of
/// the string should be highlighted.
/// </para>
/// <para>this.Text will not be null or empty when this is called.</para>
/// </remarks>
/// <param name="cellText">The text of the cell we want to search</param>
/// <returns>A list of character ranges indicating the matched substrings</returns>
abstract public IEnumerable<CharacterRange> FindAllMatchedRanges(string cellText);
/// <summary>
/// Does the given text match the filter
/// </summary>
/// <remarks>
/// <para>this.Text will not be null or empty when this is called.</para>
/// </remarks>
/// <param name="cellText">The text of the cell we want to search</param>
/// <returns>Return true if the given cellText matches our strategy</returns>
abstract public bool MatchesText(string cellText);
}
/// <summary>
/// This component provides text contains matching strategy.
/// </summary>
protected class TextContainsMatchingStrategy : TextMatchingStrategy {
/// <summary>
/// Create a text contains strategy
/// </summary>
/// <param name="filter"></param>
/// <param name="text"></param>
public TextContainsMatchingStrategy(TextMatchFilter filter, string text) {
this.TextFilter = filter;
this.Text = text;
}
/// <summary>
/// Does the given text match the filter
/// </summary>
/// <remarks>
/// <para>this.Text will not be null or empty when this is called.</para>
/// </remarks>
/// <param name="cellText">The text of the cell we want to search</param>
/// <returns>Return true if the given cellText matches our strategy</returns>
override public bool MatchesText(string cellText) {
return cellText.IndexOf(this.Text, this.StringComparison) != -1;
}
/// <summary>
/// Find all the ways in which this filter matches the given string.
/// </summary>
/// <remarks>
/// <para>
/// This is used by the renderer to decide which bits of
/// the string should be highlighted.
/// </para>
/// <para>this.Text will not be null or empty when this is called.</para>
/// </remarks>
/// <param name="cellText">The text of the cell we want to search</param>
/// <returns>A list of character ranges indicating the matched substrings</returns>
override public IEnumerable<CharacterRange> FindAllMatchedRanges(string cellText) {
List<CharacterRange> ranges = new List<CharacterRange>();
int matchIndex = cellText.IndexOf(this.Text, this.StringComparison);
while (matchIndex != -1) {
ranges.Add(new CharacterRange(matchIndex, this.Text.Length));
matchIndex = cellText.IndexOf(this.Text, matchIndex + this.Text.Length, this.StringComparison);
}
return ranges;
}
}
/// <summary>
/// This component provides text begins with matching strategy.
/// </summary>
protected class TextBeginsMatchingStrategy : TextMatchingStrategy {
/// <summary>
/// Create a text begins strategy
/// </summary>
/// <param name="filter"></param>
/// <param name="text"></param>
public TextBeginsMatchingStrategy(TextMatchFilter filter, string text) {
this.TextFilter = filter;
this.Text = text;
}
/// <summary>
/// Does the given text match the filter
/// </summary>
/// <remarks>
/// <para>this.Text will not be null or empty when this is called.</para>
/// </remarks>
/// <param name="cellText">The text of the cell we want to search</param>
/// <returns>Return true if the given cellText matches our strategy</returns>
override public bool MatchesText(string cellText) {
return cellText.StartsWith(this.Text, this.StringComparison);
}
/// <summary>
/// Find all the ways in which this filter matches the given string.
/// </summary>
/// <remarks>
/// <para>
/// This is used by the renderer to decide which bits of
/// the string should be highlighted.
/// </para>
/// <para>this.Text will not be null or empty when this is called.</para>
/// </remarks>
/// <param name="cellText">The text of the cell we want to search</param>
/// <returns>A list of character ranges indicating the matched substrings</returns>
override public IEnumerable<CharacterRange> FindAllMatchedRanges(string cellText) {
List<CharacterRange> ranges = new List<CharacterRange>();
if (cellText.StartsWith(this.Text, this.StringComparison))
ranges.Add(new CharacterRange(0, this.Text.Length));
return ranges;
}
}
/// <summary>
/// This component provides regex matching strategy.
/// </summary>
protected class TextRegexMatchingStrategy : TextMatchingStrategy {
/// <summary>
/// Creates a regex strategy
/// </summary>
/// <param name="filter"></param>
/// <param name="text"></param>
public TextRegexMatchingStrategy(TextMatchFilter filter, string text) {
this.TextFilter = filter;
this.Text = text;
}
/// <summary>
/// Gets or sets the options that will be used when compiling the regular expression.
/// </summary>
public RegexOptions RegexOptions {
get {
return this.TextFilter.RegexOptions;
}
}
/// <summary>
/// Gets or sets a compilex regular expression, based on our current Text and RegexOptions.
/// </summary>
/// <remarks>
/// If Text fails to compile as a regular expression, this will return a Regex object
/// that will match all strings.
/// </remarks>
protected Regex Regex {
get {
if (this.regex == null) {
try {
this.regex = new Regex(this.Text, this.RegexOptions);
}
catch (ArgumentException) {
this.regex = TextRegexMatchingStrategy.InvalidRegexMarker;
}
}
return this.regex;
}
set {
this.regex = value;
}
}
private Regex regex;
/// <summary>
/// Gets whether or not our current regular expression is a valid regex
/// </summary>
protected bool IsRegexInvalid {
get {
return this.Regex == TextRegexMatchingStrategy.InvalidRegexMarker;
}
}
static private Regex InvalidRegexMarker = new Regex(".*");
/// <summary>
/// Does the given text match the filter
/// </summary>
/// <remarks>
/// <para>this.Text will not be null or empty when this is called.</para>
/// </remarks>
/// <param name="cellText">The text of the cell we want to search</param>
/// <returns>Return true if the given cellText matches our strategy</returns>
public override bool MatchesText(string cellText) {
if (this.IsRegexInvalid)
return true;
return this.Regex.Match(cellText).Success;
}
/// <summary>
/// Find all the ways in which this filter matches the given string.
/// </summary>
/// <remarks>
/// <para>
/// This is used by the renderer to decide which bits of
/// the string should be highlighted.
/// </para>
/// <para>this.Text will not be null or empty when this is called.</para>
/// </remarks>
/// <param name="cellText">The text of the cell we want to search</param>
/// <returns>A list of character ranges indicating the matched substrings</returns>
override public IEnumerable<CharacterRange> FindAllMatchedRanges(string cellText) {
List<CharacterRange> ranges = new List<CharacterRange>();
if (!this.IsRegexInvalid) {
foreach (Match match in this.Regex.Matches(cellText)) {
if (match.Length > 0)
ranges.Add(new CharacterRange(match.Index, match.Length));
}
}
return ranges;
}
}
#endregion
}
}