xenadmin/XenAdmin/Controls/XenSearch/GroupingControl.cs

857 lines
32 KiB
C#
Raw Normal View History

/* Copyright (c) Citrix Systems Inc.
* All rights reserved.
*
* Redistribution and use in source and binary forms,
* with or without modification, are permitted provided
* that the following conditions are met:
*
* * Redistributions of source code must retain the above
* copyright notice, this list of conditions and the
* following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the
* following disclaimer in the documentation and/or other
* materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
using XenAdmin.CustomFields;
using XenAPI;
using XenAdmin.XenSearch;
namespace XenAdmin.Controls.XenSearch
{
public partial class GroupingControl : UserControl
{
private static readonly List<GroupingType> potentialGroups;
private static readonly FolderGroupingType folderGroupingType;
private static readonly List<CustomFieldGroupingType> customFields;
private Searcher searcher;
private NonReopeningContextMenuStrip contextMenuStrip;
private Button lastButtonClicked;
public static event EventHandler CustomFieldRemoved;
private const int MAX_GROUPS = 5;
private const int innerGutter = 6;
static GroupingControl()
{
potentialGroups = new List<GroupingType>();
XenModelObjectPropertyGroupingType<Pool> poolGroup =
new XenModelObjectPropertyGroupingType<Pool>(ObjectTypes.AllExcFolders & ~ObjectTypes.Pool, // i.e., all except Pool
PropertyNames.pool, null);
var applianceGroup = new XenModelObjectPropertyGroupingType<VM_appliance>(ObjectTypes.VM, PropertyNames.appliance, poolGroup);
XenModelObjectPropertyGroupingType<Host> hostGroup =
new XenModelObjectPropertyGroupingType<Host>(ObjectTypes.AllExcFolders & ~ObjectTypes.Pool & ~ObjectTypes.Server,
PropertyNames.host, applianceGroup);
potentialGroups.Add(poolGroup);
potentialGroups.Add(hostGroup);
potentialGroups.Add(new PropertyGroupingType<String>(ObjectTypes.VM, PropertyNames.os_name));
potentialGroups.Add(new PropertyGroupingType<vm_power_state>(ObjectTypes.VM, PropertyNames.power_state));
potentialGroups.Add(new PropertyGroupingType<VM.VirtualisationStatus>(ObjectTypes.VM, PropertyNames.virtualisation_status));
potentialGroups.Add(new PropertyGroupingType<ObjectTypes>(ObjectTypes.AllExcFolders, PropertyNames.type));
potentialGroups.Add(new XenModelObjectPropertyGroupingType<XenAPI.Network>(ObjectTypes.VM, PropertyNames.networks, poolGroup));
XenModelObjectPropertyGroupingType<SR> srGroup =
new XenModelObjectPropertyGroupingType<SR>(ObjectTypes.VM | ObjectTypes.VDI, PropertyNames.storage, poolGroup);
potentialGroups.Add(srGroup);
potentialGroups.Add(new XenModelObjectPropertyGroupingType<VDI>(ObjectTypes.VM, PropertyNames.disks, srGroup));
potentialGroups.Add(new PropertyGroupingType<VM.HA_Restart_Priority>(ObjectTypes.VM, PropertyNames.ha_restart_priority));
potentialGroups.Add(applianceGroup);
potentialGroups.Add(new PropertyGroupingType<String>(ObjectTypes.AllExcFolders, PropertyNames.tags));
potentialGroups.Add(new XenModelObjectPropertyGroupingType<VM>(
ObjectTypes.AllExcFolders & ~ObjectTypes.Pool & ~ObjectTypes.Server & ~ObjectTypes.VM,
PropertyNames.vm, hostGroup));
potentialGroups.Add(new AllCustomFieldsGroupingType());
folderGroupingType = new FolderGroupingType();
customFields = new List<CustomFieldGroupingType>();
OtherConfigAndTagsWatcher.OtherConfigChanged += OtherConfigWatcher_OtherConfigChanged;
CustomFieldsManager.CustomFieldsChanged += OtherConfigWatcher_OtherConfigChanged;
}
private static void OtherConfigWatcher_OtherConfigChanged(object sender, EventArgs e)
{
List<CustomFieldDefinition> customFieldDefinitions = CustomFieldsManager.GetCustomFields();
// Add new custom fields
foreach (CustomFieldDefinition definition in customFieldDefinitions)
if (!customFields.Exists(delegate(CustomFieldGroupingType customFieldGroupingType)
{
return customFieldGroupingType.definition.Equals(definition);
}))
{
customFields.Add(new CustomFieldGroupingType(ObjectTypes.AllExcFolders, definition));
}
// Remove old ones
foreach (CustomFieldGroupingType customFieldGroupingType in customFields.ToArray())
if (!customFieldDefinitions.Exists(delegate(CustomFieldDefinition definition)
{
return customFieldGroupingType.definition.Equals(definition);
}))
{
customFields.Remove(customFieldGroupingType);
OnCustomFieldRemoved(customFieldGroupingType);
}
}
private static void OnCustomFieldRemoved(CustomFieldGroupingType groupingType)
{
if (CustomFieldRemoved != null)
CustomFieldRemoved(groupingType, new EventArgs());
}
private readonly List<Button> groups = new List<Button>(); // Button.tag = GroupType
public GroupingControl()
{
InitializeComponent();
AddGroup(potentialGroups[0]);
CustomFieldRemoved += new EventHandler(CustomFieldRemovalWatcher_CustomFieldRemoved);
}
private void CustomFieldRemovalWatcher_CustomFieldRemoved(object sender, EventArgs e)
{
CustomFieldGroupingType groupingType = sender as CustomFieldGroupingType;
if (groupingType == null)
return;
foreach (Button button in groups.ToArray())
{
CustomFieldGroupingType groupType = button.Tag as CustomFieldGroupingType;
if (groupType == null)
continue;
if (groupType.Equals(groupingType))
{
Remove(button);
Setup();
}
}
}
public event EventHandler GroupingChanged;
protected void OnGroupChanged()
{
if (GroupingChanged != null)
GroupingChanged(this, new EventArgs());
}
private void searcher_SearchForChanged()
{
RemoveUnwantedGroups();
// If we're searching for folders, RemoveUnwantedGroups() has
// removed everything. We then add in group by folder which is
// mandatory.
if (SearchingForFolders())
AddGroup(folderGroupingType);
}
public Searcher Searcher
{
get { return searcher; }
set
{
searcher = value;
if (searcher != null)
searcher.SearchForChanged += searcher_SearchForChanged;
}
}
public Grouping Grouping
{
get
{
Grouping group = null;
Button[] buttons = groups.ToArray();
for (int i = buttons.Length - 1; i >= 0; i--)
{
Button button = buttons[i];
GroupingType groupType = button.Tag as GroupingType;
if (groupType == null)
return null;
group = groupType.GetGroup(group);
}
return group;
}
set
{
RemoveAll();
Grouping grouping = value;
while (grouping != null)
{
FromGrouping(grouping);
grouping = grouping.subgrouping;
}
Setup();
}
}
private bool SearchingForFolders()
{
return (searcher != null &&
searcher.QueryScope != null &&
searcher.QueryScope.WantType(ObjectTypes.Folder));
}
private void FromGrouping(Grouping grouping)
{
if (folderGroupingType.ForGrouping(grouping))
{
AddGroup(folderGroupingType);
return;
}
foreach (GroupingType groupType in potentialGroups)
if (groupType.ForGrouping(grouping))
{
AddGroup(groupType);
return;
}
foreach (GroupingType groupType in customFields)
if (groupType.ForGrouping(grouping))
{
AddGroup(groupType);
return;
}
// If the custom field doesn't exist, we just create a new one
CustomFieldGrouping customFieldGrouping = grouping as CustomFieldGrouping;
if (customFieldGrouping == null)
return;
AddGroup(new CustomFieldGroupingType(ObjectTypes.AllExcFolders, customFieldGrouping.definition));
}
private void Setup()
{
SuspendLayout();
bool folderSeen = false;
int offset = 0;
foreach (Button button in groups.ToArray())
{
// If the folder button is present, we must remove all other buttons.
// But the folder button is always first, so it's sufficient just to
// remove all later buttons.
if (folderSeen)
{
Remove(button);
continue;
}
if (button.Tag is FolderGroupingType)
{
folderSeen = true;
// The folder button cannot be turned off if we're searching for folders
button.Enabled = !SearchingForFolders();
}
if (!dragging || draggedButton != button)
{
button.Top = 3;
button.Left = offset;
}
offset += button.Width + innerGutter;
}
AddGroupButton.Left = offset;
OnGroupChanged();
AddGroupButton.Enabled = groups.Count < MAX_GROUPS && GetRemainingGroupTypes(null).Count > 0;
ResumeLayout();
}
// Generate a list of valid groups for a button: disallowing already-used groups
// and also ancestors of earlier buttons and descendants of later buttons.
// "context" is the button to generate the list for.
// Pass null for the Add Group button.
private List<GroupingType> GetRemainingGroupTypes(Button context)
{
List<GroupingType> remainingGroupTypes = new List<GroupingType>(potentialGroups);
foreach(GroupingType customField in customFields)
remainingGroupTypes.Add(customField);
// Remove group types which are not relevant to any of the things being searched for.
foreach (GroupingType gt in remainingGroupTypes.ToArray())
{
if (!WantGroupingType(gt))
remainingGroupTypes.Remove(gt);
}
int posRelativeToContext = -1; // -1 for before; 0 for context itself; +1 for after
foreach (Button button in groups)
{
if (button == context)
{
posRelativeToContext = 0;
}
else if (posRelativeToContext == 0)
{
posRelativeToContext = 1;
}
GroupingType groupType = button.Tag as GroupingType;
if(groupType == null)
continue;
// Remove the button type itself.
remainingGroupTypes.Remove(groupType);
// Also if we are still to the left of context, also remove ancestor types of this button;
// conversely, if we are to the right of context, remove descendant types of this button.
// Also having Folder on another button precludes all other choices.
foreach (GroupingType gt in remainingGroupTypes.ToArray())
{
if (posRelativeToContext == -1 && groupType.IsDescendantOf(gt) ||
posRelativeToContext == 1 && gt.IsDescendantOf(groupType) ||
posRelativeToContext != 0 && groupType is FolderGroupingType)
{
remainingGroupTypes.Remove(gt);
}
}
}
return remainingGroupTypes;
}
// Whether to show Folder on a button's drop-down menu
// As before, pass context==null for the Add Group Button
private bool ShowFolderOption(Button context)
{
// If this is the Add Group Button, we allow the option if there are no existing buttons
if (context == null)
return (groups.Count == 0);
// For normal buttons, if this button already shows Folder, we don't allow it on the menu again
GroupingType groupType = context.Tag as GroupingType;
if (groupType is FolderGroupingType)
return false;
// Otheriwse we allow it only on the first button
return (groups.Count != 0 && groups[0] == context);
}
private Button NewButton(GroupingType groupType)
{
Button button = new Button();
button.AutoSize = true;
button.AutoSizeMode = AutoSizeMode.GrowAndShrink;
button.Text = groupType.ToString();
button.UseVisualStyleBackColor = true;
button.TextAlign = ContentAlignment.MiddleLeft;
button.TextImageRelation = TextImageRelation.TextBeforeImage;
button.Padding = new Padding(0, 0, 2, 0);
button.Image = Properties.Resources.expanded_triangle;
button.ImageAlign = ContentAlignment.MiddleRight;
button.Tag = groupType;
button.Click += new EventHandler(button_Click);
button.MouseDown += new MouseEventHandler(button_MouseDown);
button.MouseUp += new MouseEventHandler(button_MouseUp);
button.MouseMove += new MouseEventHandler(button_MouseMove);
return button;
}
private Button draggedButton = null;
private Point clickPoint = Point.Empty;
private int dragOffset = 0;
private bool dragging = false;
void button_MouseUp(object sender, MouseEventArgs e)
{
draggedButton = null;
dragging = false;
}
void button_MouseDown(object sender, MouseEventArgs e)
{
Button button = sender as Button;
if (button == null)
return;
draggedButton = button;
clickPoint = button.PointToScreen(e.Location);
dragOffset = e.X;
}
void button_MouseMove(object sender, MouseEventArgs e)
{
if (draggedButton == null)
return;
Point OnScreen = draggedButton.PointToScreen(e.Location);
if (!dragging && Math.Abs(clickPoint.X - OnScreen.X) < 5)
return;
dragging = true;
draggedButton.BringToFront();
// Now we need to figure out where to move it,
// both on the screen and in the internal array of buttons.
// We need to take into account which buttons we have dragged it past,
// and which buttons it's allowed to pass given the grouping hierarchy.
// Note that if buttons are already out of order (legacy configuration),
// we don't force them back in order, but they can't get any worse.
GroupingType draggedGroupType = draggedButton.Tag as GroupingType;
int draggedButtonLeft = draggedButton.Left;
int draggedButtonRight = draggedButton.Left + draggedButton.Width;
int draggedButtonIndex = groups.IndexOf(draggedButton);
int offset = 0;
int leftBarrier = -1;
int swapWith = -1;
int rightBarrier = -1;
Button[] groupsArray = groups.ToArray();
foreach (Button candidateButton in groupsArray)
{
GroupingType candidateGroupType = candidateButton.Tag as GroupingType;
int candidateButtonMiddle = offset + (candidateButton.Width / 2);
int candidateButtonIndex = groups.IndexOf(candidateButton);
if (draggedGroupType != null && candidateGroupType != null)
{
if (candidateButtonIndex < draggedButtonIndex)
{
if (draggedGroupType.IsDescendantOf(candidateGroupType))
leftBarrier = candidateButtonIndex;
}
else if (candidateButtonIndex > draggedButtonIndex)
{
if (candidateGroupType.IsDescendantOf(draggedGroupType))
rightBarrier = candidateButtonIndex;
}
}
if (swapWith == -1 &&
candidateButtonMiddle > draggedButtonLeft &&
candidateButtonMiddle < draggedButtonRight)
{
swapWith = candidateButtonIndex;
}
offset += candidateButton.Width + innerGutter;
}
Point InControl = PointToClient(OnScreen);
int potentialLocation = InControl.X - dragOffset;
bool movedLeft = (swapWith >= 0 && swapWith < draggedButtonIndex);
bool movedRight = (swapWith >= 0 && swapWith > draggedButtonIndex);
bool movedNeither = !movedLeft && !movedRight;
if (movedRight || movedNeither)
{
int maxRight = 0;
if (rightBarrier >= 0)
{
if (swapWith >= 0 && swapWith >= rightBarrier)
swapWith = rightBarrier - 1;
maxRight = groupsArray[rightBarrier].Left - draggedButton.Width - innerGutter;
}
else if (groupsArray.Length >= 2)
maxRight = groupsArray[groupsArray.Length - 2].Right + innerGutter;
if (potentialLocation > maxRight)
potentialLocation = maxRight;
}
if (movedLeft || movedNeither)
{
int maxLeft = 0;
if (leftBarrier >= 0)
{
if (swapWith >= 0 && swapWith <= leftBarrier)
swapWith = leftBarrier + 1;
maxLeft = groupsArray[leftBarrier].Right + innerGutter;
}
if (potentialLocation < maxLeft)
potentialLocation = maxLeft;
}
draggedButton.Left = potentialLocation;
if (swapWith >= 0 && swapWith != draggedButtonIndex)
{
groups.Remove(draggedButton);
groups.Insert(swapWith, draggedButton);
Setup();
}
}
void AddItemToMenu(NonReopeningContextMenuStrip menu, string text, object tag, EventHandler clickHandler)
{
ToolStripMenuItem menuItem = new ToolStripMenuItem();
menuItem.Text = text;
menuItem.Tag = tag;
menuItem.Click += clickHandler;
menu.Items.Add(menuItem);
}
void button_Click(object sender, EventArgs e)
{
if (dragging)
return;
Button button = sender as Button;
if (button == null)
return;
GroupingType groupType = button.Tag as GroupingType;
if (groupType == null)
return;
if (button != lastButtonClicked)
contextMenuStrip = new NonReopeningContextMenuStrip(); // new so that it can open immediately
else if (!contextMenuStrip.CanOpen)
return;
else
contextMenuStrip.Items.Clear();
AddItemToMenu(contextMenuStrip, Messages.REMOVE_GROUPING, button,
new EventHandler(removeGroupItem_Click));
if (ShowFolderOption(button))
{
contextMenuStrip.Items.Add(new ToolStripSeparator());
AddItemToMenu(contextMenuStrip, folderGroupingType.ToString(),
new KeyValuePair<Button, GroupingType>(button, folderGroupingType),
new EventHandler(changeGroupItem_Click));
}
List<GroupingType> remainingGroupTypes = GetRemainingGroupTypes(button);
if (remainingGroupTypes.Count > 0)
{
contextMenuStrip.Items.Add(new ToolStripSeparator());
foreach (GroupingType remainingGroupType in remainingGroupTypes)
AddItemToMenu(contextMenuStrip, remainingGroupType.ToString(),
new KeyValuePair<Button, GroupingType>(button, remainingGroupType),
new EventHandler(changeGroupItem_Click));
}
lastButtonClicked = button;
contextMenuStrip.Show(this, new Point(button.Left, button.Bottom));
//Setup();
}
void removeGroupItem_Click(object sender, EventArgs e)
{
ToolStripMenuItem menuitem = sender as ToolStripMenuItem;
if (menuitem == null)
return;
Button button = menuitem.Tag as Button;
if (button == null)
return;
Remove(button);
Setup();
}
private void RemoveAll()
{
foreach (Button button in groups.ToArray())
Remove(button);
}
private void Remove(Button button)
{
groups.Remove(button);
Controls.Remove(button);
}
// Do we want this grouping type, based on the search-for?
private bool WantGroupingType(GroupingType gt)
{
QueryScope scope = (searcher == null ? null : searcher.QueryScope);
if (scope == null)
return true;
if (scope.WantType(ObjectTypes.Folder))
return false; // searching for folder forbids all grouping types (we add group by folder back in separately)
return (scope.WantAnyOf(gt.AppliesTo));
}
private bool WantGroupingType(Button button)
{
GroupingType gt = button.Tag as GroupingType;
return WantGroupingType(gt);
}
// Remove groups which don't match the current search-for
private void RemoveUnwantedGroups()
{
foreach (Button button in groups.ToArray())
{
if (!WantGroupingType(button))
Remove(button);
}
Setup();
}
void changeGroupItem_Click(object sender, EventArgs e)
{
ToolStripMenuItem menuitem = sender as ToolStripMenuItem;
if (menuitem == null)
return;
if(!(menuitem.Tag is KeyValuePair<Button, GroupingType>))
return;
KeyValuePair<Button, GroupingType> kvp = (KeyValuePair<Button, GroupingType>) menuitem.Tag;
kvp.Key.Tag = kvp.Value;
kvp.Key.Text = kvp.Value.ToString();
Setup();
}
private void AddGroupButton_Click(object sender, EventArgs e)
{
if (groups.Count >= MAX_GROUPS)
return;
if (AddGroupButton != lastButtonClicked)
contextMenuStrip = new NonReopeningContextMenuStrip(); // new so that it can open immediately
else if (!contextMenuStrip.CanOpen)
return;
else
contextMenuStrip.Items.Clear();
if (ShowFolderOption(null))
{
AddItemToMenu(contextMenuStrip, folderGroupingType.ToString(), folderGroupingType,
new EventHandler(addGroupItem_Click));
contextMenuStrip.Items.Add(new ToolStripSeparator());
}
foreach (GroupingType groupType in GetRemainingGroupTypes(null))
AddItemToMenu(contextMenuStrip, groupType.ToString(), groupType,
new EventHandler(addGroupItem_Click));
lastButtonClicked = AddGroupButton;
contextMenuStrip.Show(this, new Point(AddGroupButton.Left, AddGroupButton.Bottom));
}
void addGroupItem_Click(object sender, EventArgs e)
{
ToolStripMenuItem menuItem = sender as ToolStripMenuItem;
if (menuItem == null)
return;
GroupingType groupType = menuItem.Tag as GroupingType;
if (groupType == null)
return;
AddGroup(groupType);
}
void AddGroup(GroupingType groupType)
{
Button button = NewButton(groupType);
groups.Add(button);
Controls.Add(button);
Setup();
}
public abstract class GroupingType
{
private readonly ObjectTypes appliesTo;
protected GroupingType(ObjectTypes appliesTo)
{
this.appliesTo = appliesTo;
}
public ObjectTypes AppliesTo
{
get { return appliesTo; }
}
public abstract Grouping GetGroup(Grouping subgrouping);
public abstract bool ForGrouping(Grouping grouping);
public virtual bool IsDescendantOf(GroupingType gt)
{
return false;
}
}
public class FolderGroupingType : GroupingType
{
public FolderGroupingType()
: base(ObjectTypes.AllIncFolders)
{
}
public override Grouping GetGroup(Grouping subgrouping)
{
return new FolderGrouping(subgrouping);
}
public override bool ForGrouping(Grouping grouping)
{
return grouping is FolderGrouping;
}
public override string ToString()
{
return Messages.FOLDER;
}
}
public class PropertyGroupingType<T> : GroupingType
{
protected readonly PropertyNames property;
protected readonly String i18n;
public PropertyGroupingType(ObjectTypes appliesTo, PropertyNames property)
: base(appliesTo)
{
this.property = property;
this.i18n = PropertyAccessors.PropertyNames_i18n[property];
}
public override string ToString()
{
return i18n;
}
public override Grouping GetGroup(Grouping subgrouping)
{
return new PropertyGrouping<T>(property, subgrouping);
}
public override bool ForGrouping(Grouping grouping)
{
PropertyGrouping<T> propertyGrouping = grouping as PropertyGrouping<T>;
if (propertyGrouping == null)
return false;
return propertyGrouping.property == property;
}
}
public class XenModelObjectPropertyGroupingType<T>
: PropertyGroupingType<T> where T : XenObject<T>
{
protected readonly GroupingType parent; // the GroupingType next up in the tree: null for the top of the tree
public XenModelObjectPropertyGroupingType(ObjectTypes appliesTo, PropertyNames property, GroupingType parent)
: base(appliesTo, property)
{
this.parent = parent;
}
public override Grouping GetGroup(Grouping subgrouping)
{
return new XenModelObjectPropertyGrouping<T>(property, subgrouping);
}
public override bool IsDescendantOf(GroupingType gt)
{
return (gt == parent || (parent != null && parent.IsDescendantOf(gt)));
}
}
public class CustomFieldGroupingType : GroupingType
{
public readonly CustomFieldDefinition definition;
public CustomFieldGroupingType(ObjectTypes appliesTo, CustomFieldDefinition definition)
: base(appliesTo)
{
this.definition = definition;
}
public override string ToString()
{
return definition.Name;
}
public override Grouping GetGroup(Grouping subgrouping)
{
return new CustomFieldGrouping(definition, subgrouping);
}
public override bool ForGrouping(Grouping grouping)
{
CustomFieldGrouping customFieldGrouping = grouping as CustomFieldGrouping;
if (customFieldGrouping == null)
return false;
return customFieldGrouping.definition.Equals(definition);
}
}
public class AllCustomFieldsGroupingType : GroupingType
{
public AllCustomFieldsGroupingType()
: base(ObjectTypes.AllExcFolders)
{
}
public override Grouping GetGroup(Grouping subgrouping)
{
return new AllCustomFieldsGrouping(subgrouping);
}
public override bool ForGrouping(Grouping grouping)
{
return grouping is AllCustomFieldsGrouping;
}
public override string ToString()
{
return Messages.CUSTOM_FIELDS;
}
}
}
}