/* 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.ComponentModel; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; using XenAdmin.Controls; using XenAdmin.Core; using XenAdmin.Dialogs; using XenAdmin.Network; using XenAPI; using XenAdmin.Alerts; using XenAdmin.Help; using System.Threading; using XenAdmin.Actions; using System.IO; namespace XenAdmin.TabPages { public partial class AlertSummaryPage : UserControl { private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); private static readonly int ALERT_CAP = 1000; private readonly CollectionChangeEventHandler m_alertCollectionChangedWithInvoke; Dictionary expandedState = new Dictionary(); private bool inAlertBuild; private bool retryAlertBuild; public AlertSummaryPage() { InitializeComponent(); GridViewAlerts.Sort(ColumnDate, ListSortDirection.Descending); LabelCappingEntries.Text = String.Format(Messages.ALERT_CAP_LABEL, ALERT_CAP); GridViewAlerts.ScrollBars = ScrollBars.Vertical; UpdateActionEnablement(); m_alertCollectionChangedWithInvoke = Program.ProgramInvokeHandler(AlertsCollectionChanged); Alert.RegisterAlertCollectionChanged(m_alertCollectionChangedWithInvoke); toolStripSplitButtonDismiss.DefaultItem = tsmiDismissAll; toolStripSplitButtonDismiss.Text = tsmiDismissAll.Text; } public void RefreshAlertList() { toolStripDropDownButtonServerFilter.InitializeHostList(); toolStripDropDownButtonServerFilter.BuildFilterList(); Rebuild(); } private void SetFilterLabel() { toolStripLabelFiltersOnOff.Text = FilterIsOn ? Messages.FILTERS_ON : Messages.FILTERS_OFF; } private bool FilterIsOn { get { return toolStripDropDownButtonDateFilter.FilterIsOn || toolStripDropDownButtonServerFilter.FilterIsOn || toolStripDropDownSeveritiesFilter.FilterIsOn; } } #region AlertListCode private void Rebuild() { log.Debug("Rebuilding alertList"); Thread t = new Thread(_Rebuild); t.Name = "Building alert list"; t.IsBackground = true; t.Start(); } private void _Rebuild() { log.Debug("Rebuilding alertList: Starting background thread"); Program.AssertOffEventThread(); lock (GridViewAlerts) { if (inAlertBuild) { // queue up a rebuild after the current one has completed log.Debug("Rebuilding alertList: In build already, exiting"); retryAlertBuild = true; return; } inAlertBuild = true; log.Debug("Rebuilding alertList: taking inAlertBuild lock"); } try { // 1) Add all the alerts that have not been filtered out to an array // 2) Create rows for each of these // 3) Sort them // 4) Take the top n as set by the filters // 5) Add them to the control using the optimized AddRange() Program.Invoke(this, SetFilterLabel); List alerts = Alert.NonDismissingAlerts; alerts.RemoveAll(FilterAlert); log.DebugFormat("Rebuilding alertList: there are {0} alerts in total. After filtering we have {1}", Alert.AlertCount, alerts.Count); if (GridViewAlerts.SortedColumn != null) { if (GridViewAlerts.SortedColumn.Index == ColumnMessage.Index) { alerts.Sort(Alert.CompareOnTitle); } else if (GridViewAlerts.SortedColumn.Index == ColumnDate.Index) { alerts.Sort(Alert.CompareOnDate); } else if (GridViewAlerts.SortedColumn.Index == ColumnLocation.Index) { alerts.Sort(Alert.CompareOnAppliesTo); } else if (GridViewAlerts.SortedColumn.Index == ColumnSeverity.Index) { alerts.Sort(Alert.CompareOnPriority); } if (GridViewAlerts.SortOrder == SortOrder.Descending) { alerts.Reverse(); } } int alertsFound = alerts.Count; if (ALERT_CAP < alerts.Count) { log.DebugFormat("Rebuilding alertList: hit alert cap, hiding {0} alerts", alerts.Count - ALERT_CAP); alerts.RemoveRange(ALERT_CAP, alerts.Count - ALERT_CAP); } Program.Invoke(this, delegate { List gridRows = new List(); log.Debug("Rebuilding alertList: Adding alert rows"); foreach (Alert alert in alerts) gridRows.Add(NewAlertRow(alert)); log.DebugFormat("Rebuilding alertList: Added {0} rows", gridRows.Count); List selection = (GridViewAlerts.SelectedRows.Cast().Select( selectedRow => ((Alert)selectedRow.Tag).uuid)).ToList(); GridViewAlerts.Rows.Clear(); log.Debug("Rebuilding alertList: Cleared rows"); GridViewAlerts.Rows.AddRange(gridRows.ToArray()); log.DebugFormat("Rebuilding alertList: Added {0} rows to the grid", GridViewAlerts.Rows.Count); tableLayoutPanel3.Visible = alertsFound > ALERT_CAP; //restore selection if (selection.Count > 0) { log.Debug("Rebuilding alertList: Restoring alert selection"); foreach (DataGridViewRow alertRow in GridViewAlerts.Rows) { alertRow.Selected = selection.Contains(((Alert)alertRow.Tag).uuid); } if (GridViewAlerts.SelectedRows.Count == 0 && GridViewAlerts.Rows.Count > 0) { GridViewAlerts.Rows[0].Selected = true; } log.DebugFormat("Rebuilding alertList: Selected {0} alerts", selection.Count); } UpdateActionEnablement(); }); } catch (Exception e) { log.ErrorFormat("Encountered exception when building list: {0}", e); } finally { log.Debug("Rebuilding alertList: Waiting for lock to clear inAlertBuild"); lock (GridViewAlerts) { inAlertBuild = false; log.Debug("Rebuilding alertList: cleared inAlertBuild"); if (retryAlertBuild) { // we received a request to build while we were building, rebuild in case we missed something retryAlertBuild = false; log.Debug("Rebuilding alertList: we received a request to build while we were building, rebuild in case we missed something"); _Rebuild(); } } } } private DataGridViewRow NewAlertRow(Alert alert) { var expanderCell = new DataGridViewImageCell(); var imageCell = new DataGridViewImageCell(); var appliesCell = new DataGridViewTextBoxCell(); var detailCell = new DataGridViewTextBoxCell(); var dateCell = new DataGridViewTextBoxCell(); var actionItems = GetAlertActionItems(alert); var actionCell = new DataGridViewDropDownSplitButtonCell(actionItems.ToArray()); var newRow = new DataGridViewRow { Tag = alert, MinimumHeight = DataGridViewDropDownSplitButtonCell.MIN_ROW_HEIGHT }; // Get the relevant image for the row depending on the type of the alert Image typeImage = alert is MessageAlert && ((MessageAlert)alert).Message.ShowOnGraphs ? Images.GetImage16For(((MessageAlert)alert).Message.Type) : Images.GetImage16For(alert.Priority); imageCell.Value = typeImage; // Set the detail cell content and expanding arrow if (expandedState.ContainsKey(alert.uuid)) { // show the expanded arrow and the body detail expanderCell.Value = Properties.Resources.expanded_triangle; detailCell.Value = String.Format("{0}\n\n{1}", alert.Title, alert.Description); } else { // show the expand arrow and just the title expanderCell.Value = Properties.Resources.contracted_triangle; detailCell.Value = alert.Title; } appliesCell.Value = alert.AppliesTo; dateCell.Value = HelpersGUI.DateTimeToString(alert.Timestamp.ToLocalTime(), Messages.DATEFORMAT_DMY_HM, true); newRow.Cells.AddRange(expanderCell, imageCell, detailCell, appliesCell, dateCell, actionCell); return newRow; } /// /// Runs all the current filters on the alert to determine if it should be shown in the list or not. /// /// private bool FilterAlert(Alert alert) { bool hide = false; Program.Invoke(this, () => hide = toolStripDropDownButtonDateFilter.HideByDate(alert.Timestamp.ToLocalTime()) || toolStripDropDownButtonServerFilter.HideByLocation(alert.HostUuid) || toolStripDropDownSeveritiesFilter.HideBySeverity(alert.Priority)); return hide; } private void RemoveAlertRow(Alert a) { for (int i = 0; i < GridViewAlerts.Rows.Count; i++) { if (((Alert)GridViewAlerts.Rows[i].Tag).uuid == a.uuid) GridViewAlerts.Rows.RemoveAt(i); } if (GridViewAlerts.Rows.Count < ALERT_CAP) tableLayoutPanel3.Visible = false; } private void GridViewAlerts_CellClick(object sender, DataGridViewCellEventArgs e) { // If you click on the headers you can get -1 as the index. if (e.ColumnIndex < 0 || e.RowIndex < 0 || e.ColumnIndex != ColumnExpand.Index) return; ToggleExpandedState(e.RowIndex); } private void GridViewAlerts_CellDoubleClick(object sender, DataGridViewCellEventArgs e) { // If you click on the headers you can get -1 as the index. if (e.ColumnIndex < 0 || e.RowIndex < 0) return; if (e.ColumnIndex != ColumnActions.Index) ToggleExpandedState(e.RowIndex); } private void GridViewAlerts_KeyDown(object sender, KeyEventArgs e) { if (e.KeyCode == Keys.Right) // expand all selected rows { foreach (DataGridViewBand row in GridViewAlerts.SelectedRows) { Alert alert = (Alert)GridViewAlerts.Rows[row.Index].Tag; if (!expandedState.ContainsKey(alert.uuid)) { ToggleExpandedState(row.Index); } } } else if (e.KeyCode == Keys.Left) // collapse all selected rows { foreach (DataGridViewBand row in GridViewAlerts.SelectedRows) { Alert alert = (Alert)GridViewAlerts.Rows[row.Index].Tag; if (expandedState.ContainsKey(alert.uuid)) { ToggleExpandedState(row.Index); } } } else if (e.KeyCode == Keys.Enter) // toggle expanded state for all selected rows { foreach (DataGridViewBand row in GridViewAlerts.SelectedRows) { ToggleExpandedState(row.Index); } } } private void GridViewAlerts_ColumnHeaderMouseClick(object sender, DataGridViewCellMouseEventArgs e) { if (GridViewAlerts.Columns[e.ColumnIndex].SortMode == DataGridViewColumnSortMode.Automatic) { Rebuild(); } } private void GridViewAlerts_SelectionChanged(object sender, EventArgs e) { // stop the buttons getting enabled/disabled during refresh, the rebuild will set them once it's finished if (inAlertBuild) return; UpdateActionEnablement(); } /// /// Handles the automatic sorting of the AlertsGridView for the non-string columns /// /// /// private void GridViewAlerts_SortCompare(object sender, DataGridViewSortCompareEventArgs e) { Alert alert1 = (Alert)GridViewAlerts.Rows[e.RowIndex1].Tag; Alert alert2 = (Alert)GridViewAlerts.Rows[e.RowIndex2].Tag; if (e.Column.Index == ColumnDate.Index) { int SortResult = DateTime.Compare(alert1.Timestamp, alert2.Timestamp); e.SortResult = (GridViewAlerts.SortOrder == SortOrder.Descending) ? SortResult *= -1 : SortResult; e.Handled = true; } else if (e.Column.Index == ColumnSeverity.Index) { e.SortResult = Alert.CompareOnPriority(alert1, alert2); e.Handled = true; } } private void ToggleExpandedState(int rowIndex) { var row = GridViewAlerts.Rows[rowIndex]; Alert alert = row.Tag as Alert; if (alert == null) return; if (expandedState.ContainsKey(alert.uuid)) { expandedState.Remove(alert.uuid); row.Cells[ColumnExpand.Index].Value = Properties.Resources.contracted_triangle; row.Cells[ColumnMessage.Index].Value = alert.Title; } else { expandedState.Add(alert.uuid, true); row.Cells[ColumnExpand.Index].Value = Properties.Resources.expanded_triangle; row.Cells[ColumnMessage.Index].Value = string.Format("{0}\n\n{1}", alert.Title, alert.Description); } } #endregion private void ToolStripMenuItemHelp_Click(object sender, EventArgs e) { // We should only be here if one item is selected, we dont do multi-help if (GridViewAlerts.SelectedRows.Count != 1) log.ErrorFormat("Can only launch help for 1 alert at a time (Attempted to launch {0}). Launching for the first one in the list)", GridViewAlerts.SelectedRows.Count); Alert alert = (Alert)GridViewAlerts.SelectedRows[0].Tag; if (alert.HelpID == null) { log.ErrorFormat("Attempted to launch help for alert {0} ({1}) but no helpID available. Not launching.", alert.Title, alert.uuid); return; } HelpManager.Launch(alert.HelpID); } private void ToolStripMenuItemFix_Click(object sender, EventArgs e) { // We should only be here if one item is selected, we dont do multi-fix if (GridViewAlerts.SelectedRows.Count == 0) { log.ErrorFormat("Attempted to fix alert with no alert selected."); return; } if (GridViewAlerts.SelectedRows.Count != 1) log.ErrorFormat("Only 1 alert can be fixed at a time (Attempted to fix {0}). Fixing the first one in the list.", GridViewAlerts.SelectedRows.Count); Alert alert = (Alert)GridViewAlerts.SelectedRows[0].Tag; if (alert.FixLinkAction == null) { log.ErrorFormat("Attempted to fix alert {0} ({1}) but no fix link action available. Not fixing.", alert.Title, alert.uuid); return; } alert.FixLinkAction.Invoke(); } private void ToolStripMenuItemDismiss_Click(object sender, EventArgs e) { using (var dlog = new ThreeButtonDialog( new ThreeButtonDialog.Details(null, Messages.ALERT_DISMISS_CONFIRM, Messages.XENCENTER), ThreeButtonDialog.ButtonYes, ThreeButtonDialog.ButtonNo)) { if (dlog.ShowDialog(this) != DialogResult.Yes) return; } var selectedAlerts = from DataGridViewRow row in GridViewAlerts.SelectedRows select row.Tag as Alert; DismissAlerts(selectedAlerts); } private void tsmiDismissAll_Click(object sender, EventArgs e) { DialogResult result; if (!FilterIsOn) { using (var dlog = new ThreeButtonDialog( new ThreeButtonDialog.Details(null, Messages.ALERT_DISMISS_ALL_NO_FILTER_CONTINUE), new ThreeButtonDialog.TBDButton(Messages.DISMISS_ALL_YES_CONFIRM_BUTTON, DialogResult.Yes), ThreeButtonDialog.ButtonCancel)) { result = dlog.ShowDialog(this); } } else { using (var dlog = new ThreeButtonDialog( new ThreeButtonDialog.Details(null, Messages.ALERT_DISMISS_ALL_CONTINUE), new ThreeButtonDialog.TBDButton(Messages.DISMISS_ALL_CONFIRM_BUTTON, DialogResult.Yes), new ThreeButtonDialog.TBDButton(Messages.DISMISS_FILTERED_CONFIRM_BUTTON, DialogResult.No, ThreeButtonDialog.ButtonType.NONE), ThreeButtonDialog.ButtonCancel)) { result = dlog.ShowDialog(this); } } if (result == DialogResult.Cancel) return; var alerts = result == DialogResult.No ? (from DataGridViewRow row in GridViewAlerts.Rows select row.Tag as Alert) : Alert.Alerts; DismissAlerts(alerts); } private void AlertsCollectionChanged(object sender, CollectionChangeEventArgs e) { Program.AssertOnEventThread(); if (e.Element == null) { // We take the null element to mean there has been a batch remove Rebuild(); return; } Alert a = e.Element as Alert; switch (e.Action) { case CollectionChangeAction.Add: Rebuild(); // rebuild entire alert list to ensure filtering and sorting break; case CollectionChangeAction.Remove: RemoveAlertRow(a); break; } } private void UpdateActionEnablement() { toolStripDropDownSeveritiesFilter.Enabled = toolStripDropDownButtonServerFilter.Enabled = toolStripDropDownButtonDateFilter.Enabled = toolStripButtonExportAll.Enabled = Alert.NonDismissingAlertCount > 0; tsmiDismissAll.Enabled = AllowedToDismiss(Alert.Alerts); tsmiDismissAll.AutoToolTip = !tsmiDismissAll.Enabled; tsmiDismissAll.ToolTipText = tsmiDismissAll.Enabled ? string.Empty : Alert.NonDismissingAlertCount > 0 ? Messages.DELETE_ANY_MESSAGE_RBAC_BLOCKED : Messages.NO_MESSAGES_TO_DISMISS; var selectedAlerts = from DataGridViewRow row in GridViewAlerts.SelectedRows select row.Tag as Alert; tsmiDismissSelected.Enabled = AllowedToDismiss(selectedAlerts); tsmiDismissSelected.AutoToolTip = !tsmiDismissSelected.Enabled; tsmiDismissSelected.ToolTipText = tsmiDismissSelected.Enabled ? string.Empty : Messages.DELETE_MESSAGE_RBAC_BLOCKED; toolStripSplitButtonDismiss.Enabled = tsmiDismissAll.Enabled || tsmiDismissSelected.Enabled; if (toolStripSplitButtonDismiss.DefaultItem != null && !toolStripSplitButtonDismiss.DefaultItem.Enabled) { foreach (ToolStripItem item in toolStripSplitButtonDismiss.DropDownItems) { if (item.Enabled) { toolStripSplitButtonDismiss.DefaultItem = item; toolStripSplitButtonDismiss.Text = item.Text; break; } } } } #region Alert dismissal private bool AllowedToDismiss(Alert alert) { return AllowedToDismiss(new[] { alert }); } private bool AllowedToDismiss(IEnumerable alerts) { var alertConnections = (from Alert alert in alerts where alert != null && !alert.Dismissing let con = alert.Connection select con).Distinct(); if (alertConnections.Count() == 0) return false; return alertConnections.Any(AllowedToDismiss); } /// /// Checks the user has sufficient RBAC privileges to clear alerts on a given connection /// private bool AllowedToDismiss(IXenConnection c) { // check if local alert if (c == null) return true; // have we disconnected? Alert will disappear soon, but for now block dismissal. if (c.Session == null) return false; if (c.Session.IsLocalSuperuser || !Helpers.MidnightRideOrGreater(c)) return true; List rolesAbleToCompleteAction = Role.ValidRoleList("Message.destroy", c); foreach (Role possibleRole in rolesAbleToCompleteAction) { if (c.Session.Roles.Contains(possibleRole)) return true; } return false; } private void DismissAlerts(IEnumerable alerts) { var groups = from Alert alert in alerts where alert != null && !alert.Dismissing group alert by alert.Connection into g select new { Connection = g.Key, Alerts = g }; foreach (var g in groups) { if (AllowedToDismiss(g.Connection)) new DeleteAllAlertsAction(g.Connection, g.Alerts).RunAsync(); } Rebuild(); } #endregion private List GetAlertActionItems(Alert alert) { var items = new List(); if (AllowedToDismiss(alert)) { var dismiss = new ToolStripMenuItem(Messages.ALERT_DISMISS); dismiss.Click += ToolStripMenuItemDismiss_Click; items.Add(dismiss); } if (!string.IsNullOrEmpty(alert.FixLinkText) && alert.FixLinkAction != null) { var fix = new ToolStripMenuItem(alert.FixLinkText); fix.Click += ToolStripMenuItemFix_Click; items.Add(fix); } if (!string.IsNullOrEmpty(alert.HelpID)) { var help = new ToolStripMenuItem(alert.HelpLinkText); help.Click += ToolStripMenuItemHelp_Click; items.Add(help); } if (items.Count > 0) items.Add(new ToolStripSeparator()); var copy = new ToolStripMenuItem(Messages.COPY); copy.Click += copyToolStripMenuItem_Click; items.Add(copy); return items; } #region Top ToolStrip event handlers private void toolStripDropDownButtonDateFilter_FilterChanged() { Rebuild(); } private void toolStripDropDownButtonServerFilter_FilterChanged() { Rebuild(); } private void toolStripDropDownSeveritiesFilter_FilterChanged() { Rebuild(); } private void toolStripButtonRefresh_Click(object sender, EventArgs e) { Rebuild(); } private void toolStripButtonExportAll_Click(object sender, EventArgs e) { bool exportAll = true; if (FilterIsOn) { using (var dlog = new ThreeButtonDialog( new ThreeButtonDialog.Details(null, Messages.ALERT_EXPORT_ALL_OR_FILTERED), new ThreeButtonDialog.TBDButton(Messages.ALERT_EXPORT_ALL_BUTTON, DialogResult.Yes), new ThreeButtonDialog.TBDButton(Messages.ALERT_EXPORT_FILTERED_BUTTON, DialogResult.No, ThreeButtonDialog.ButtonType.NONE), ThreeButtonDialog.ButtonCancel)) { var result = dlog.ShowDialog(this); if (result == DialogResult.No) exportAll = false; else if (result == DialogResult.Cancel) return; } } string fileName; using (SaveFileDialog dialog = new SaveFileDialog { AddExtension = true, Filter = string.Format("{0} (*.csv)|*.csv|{1} (*.*)|*.*", Messages.CSV_DESCRIPTION, Messages.ALL_FILES), FilterIndex = 0, Title = Messages.EXPORT_ALL, RestoreDirectory = true, DefaultExt = "csv", CheckPathExists = false, OverwritePrompt = true }) { if (dialog.ShowDialog(this) != DialogResult.OK) return; fileName = dialog.FileName; } new DelegatedAsyncAction(null, string.Format(Messages.EXPORT_SYSTEM_ALERTS, fileName), string.Format(Messages.EXPORTING_SYSTEM_ALERTS, fileName), string.Format(Messages.EXPORTED_SYSTEM_ALERTS, fileName), delegate { using (StreamWriter stream = new StreamWriter(fileName, false, UTF8Encoding.UTF8)) { stream.WriteLine("{0},{1},{2},{3},{4}", Messages.TITLE, Messages.SEVERITY, Messages.DESCRIPTION, Messages.APPLIES_TO, Messages.TIMESTAMP); if (exportAll) { foreach (Alert a in Alert.Alerts) { if (!a.Dismissing) stream.WriteLine(a.GetAlertDetailsCSVQuotes()); } } else { foreach (DataGridViewRow row in GridViewAlerts.Rows) { var a = row.Tag as Alert; if (a != null && !a.Dismissing) stream.WriteLine(a.GetAlertDetailsCSVQuotes()); } } } }).RunAsync(); } private void toolStripSplitButtonDismiss_DropDownItemClicked(object sender, ToolStripItemClickedEventArgs e) { toolStripSplitButtonDismiss.DefaultItem = e.ClickedItem; toolStripSplitButtonDismiss.Text = toolStripSplitButtonDismiss.DefaultItem.Text; } #endregion private void copyToolStripMenuItem_Click(object sender, EventArgs e) { // We should only be here if one item is selected, we don't do multi-fix if (GridViewAlerts.SelectedRows.Count == 0) { log.ErrorFormat("Attempted to copy alert with no alert selected."); return; } StringBuilder sb = new StringBuilder(); foreach (DataGridViewRow r in GridViewAlerts.SelectedRows) { Alert alert = (Alert)r.Tag; sb.AppendLine(alert.GetAlertDetailsCSVQuotes()); } try { Clipboard.SetText(sb.ToString()); } catch (Exception ex) { log.Error("Exception while trying to set clipboard text.", ex); log.Error(ex, ex); } } } }