xenadmin/XenAdmin/TabPages/SrStoragePage.cs
Konstantina Chremmou ad7e2fa6a6 CA-257927: Changed ChangeableList.RemoveAll method to fire the CollectionChangedEvent
once for all the items to be removed, passing in their list as argument. This method,
and the method Clear calling it, are used for alerts, updates, events and connections,
hence changed the handling of the CollectionChanged event in these cases to expect
either a single item or a collection. In the case of alerts, updates and events
this means that we can rebuild the whole list instead of removing single rows which
causes the application to hang for big numbers of items.
2017-07-24 14:48:02 +01:00

765 lines
26 KiB
C#

/* 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.Windows.Forms;
using XenAdmin.Controls;
using XenAdmin.Core;
using XenAdmin.Actions;
using XenAPI;
using XenAdmin.Dialogs;
using XenAdmin.Commands;
namespace XenAdmin.TabPages
{
internal partial class SrStoragePage : BaseTabPage
{
private SR sr;
private bool rebuildRequired;
private readonly VDIsDataGridViewBuilder dataGridViewBuilder;
public SrStoragePage()
{
InitializeComponent();
for (int i = 0; i < 5; i++)
{
dataGridViewVDIs.Columns[i].SortMode = DataGridViewColumnSortMode.Automatic;
}
ConnectionsManager.History.CollectionChanged += History_CollectionChanged;
base.Text = Messages.VIRTUAL_DISKS;
Properties.Settings.Default.PropertyChanged += Default_PropertyChanged;
dataGridViewBuilder = new VDIsDataGridViewBuilder(this);
}
private bool disposed;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (!disposed)
{
// Deregister listeners.
SR = null;
ConnectionsManager.History.CollectionChanged -= History_CollectionChanged;
Properties.Settings.Default.PropertyChanged -= Default_PropertyChanged;
if (disposing)
{
if (components != null)
components.Dispose();
dataGridViewBuilder.Stop();
}
disposed = true;
}
base.Dispose(disposing);
}
private void SetupDeprecationBanner()
{
if (sr != null && SR.IsIslOrIslLegacy(sr))
{
Banner.AppliesToVersion = Messages.XENSERVER_6_5;
Banner.BannerType = DeprecationBanner.Type.Removal;
Banner.FeatureName = Messages.ISL_SR;
Banner.LinkUri = HiddenFeatures.LinkLabelHidden ? null : new Uri(InvisibleMessages.ISL_DEPRECATION_URL);
Banner.Visible = !HiddenFeatures.LinkLabelHidden;
}
else
Banner.Visible = false;
}
public SR SR
{
set
{
Program.AssertOnEventThread();
if (sr != null)
{
sr.PropertyChanged -= sr_PropertyChanged;
sr.Connection.Cache.DeregisterBatchCollectionChanged<VDI>(VDI_BatchCollectionChanged);
sr.Connection.XenObjectsUpdated -= Connection_XenObjectsUpdated;
}
sr = value;
if (sr != null)
{
sr.PropertyChanged += sr_PropertyChanged;
sr.Connection.Cache.RegisterBatchCollectionChanged<VDI>(VDI_BatchCollectionChanged);
addVirtualDiskButton.Visible = sr.SupportsVdiCreate();
sr.Connection.XenObjectsUpdated += Connection_XenObjectsUpdated;
}
BuildList(true);
SetupDeprecationBanner();
}
}
private void RefreshDataGridView(VDIsData data)
{
dataGridViewVDIs.SuspendLayout();
try
{
ColumnVolume.Visible = data.ShowStorageLink;
// Update existing rows
foreach (var vdiRow in data.VdiRowsToUpdate)
{
vdiRow.RefreshRowDetails();
}
// Remove rows for deleted VDIs
foreach (var vdiRow in data.VdiRowsToRemove)
{
dataGridViewVDIs.Rows.RemoveAt(vdiRow.Index);
}
// Add rows for new VDIs
foreach (var vdi in data.VdisToAdd)
{
dataGridViewVDIs.Rows.Add(new VDIRow(vdi));
}
}
finally
{
if (dataGridViewVDIs.SortedColumn != null && dataGridViewVDIs.SortOrder != SortOrder.None)
dataGridViewVDIs.Sort(dataGridViewVDIs.SortedColumn,
dataGridViewVDIs.SortOrder == SortOrder.Ascending
? ListSortDirection.Ascending
: ListSortDirection.Descending);
dataGridViewVDIs.ResumeLayout();
}
RefreshButtons();
}
private IEnumerable<VDIRow> GetCurrentVDIRows()
{
return dataGridViewVDIs.Rows.OfType<VDIRow>();
}
private void BuildList(bool reset)
{
Program.AssertOnEventThread();
if (sr == null)
return;
dataGridViewBuilder.AddRequest(new RefreshGridRequest(sr, reset));
}
private SelectedItemCollection SelectedVDIs
{
get
{
List<SelectedItem> vdis = new List<SelectedItem>();
foreach (DataGridViewRow r in dataGridViewVDIs.SelectedRows)
{
VDIRow row = r as VDIRow;
if (row != null)
vdis.Add(new SelectedItem(row.VDI));
}
return new SelectedItemCollection(vdis);
}
}
private void UnregisterHandlers()
{
ConnectionsManager.History.CollectionChanged -= History_CollectionChanged;
Properties.Settings.Default.PropertyChanged -= Default_PropertyChanged;
if (sr != null)
{
sr.PropertyChanged -= sr_PropertyChanged;
sr.Connection.Cache.DeregisterBatchCollectionChanged<VDI>(VDI_BatchCollectionChanged);
sr.Connection.XenObjectsUpdated -= Connection_XenObjectsUpdated;
}
}
public override void PageHidden()
{
UnregisterHandlers();
}
#region events
void History_CollectionChanged(object sender, CollectionChangeEventArgs e)
{
Program.BeginInvoke(Program.MainWindow, () =>
{
SrRefreshAction a = e.Element as SrRefreshAction;
if (e.Action == CollectionChangeAction.Add)
{
if (a != null)
a.Completed += a_Completed;
else
return;
}
if (e.Action == CollectionChangeAction.Remove)
{
if (a != null)
{
a.Completed -= a_Completed;
}
else
{
var range = e.Element as List<SrRefreshAction>;
if (range != null)
{
foreach (var sra in range)
sra.Completed -= a_Completed;
}
else
{
return;
}
}
}
RefreshButtons();
});
}
void a_Completed(ActionBase sender)
{
Program.Invoke(Program.MainWindow, RefreshButtons);
}
void Connection_XenObjectsUpdated(object sender, EventArgs e)
{
if (rebuildRequired)
BuildList(false);
rebuildRequired = false;
}
void sr_PropertyChanged(object sender1, PropertyChangedEventArgs e)
{
if (e.PropertyName == "VDIs")
rebuildRequired = true;
}
private void VDI_BatchCollectionChanged(object sender, EventArgs e)
{
rebuildRequired = true;
}
private void Default_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName != "ShowHiddenVMs")
return;
Program.Invoke(this, () => BuildList(false));
}
#endregion
#region datagridvie wevents
private void DataGridViewObject_SortCompare(object sender, DataGridViewSortCompareEventArgs e)
{
if (e.Column.Index == ColumnName.Index)
{
var vdi1 = ((VDIRow) dataGridViewVDIs.Rows[e.RowIndex1]).VDI;
var vdi2 = ((VDIRow) dataGridViewVDIs.Rows[e.RowIndex2]).VDI;
e.SortResult = vdi1.CompareTo(vdi2);
e.Handled = true;
return;
}
if (e.Column.Index == ColumnDesc.Index)
{
var vdi1 = ((VDIRow)dataGridViewVDIs.Rows[e.RowIndex1]).VDI;
var vdi2 = ((VDIRow)dataGridViewVDIs.Rows[e.RowIndex2]).VDI;
var descCompare = StringUtility.NaturalCompare(vdi1.Description, vdi2.Description);
if (descCompare != 0)
{
e.SortResult = descCompare;
}
else
{
var refCompare = string.Compare(vdi1.opaque_ref, vdi2.opaque_ref, StringComparison.Ordinal);
e.SortResult = refCompare;
}
e.Handled = true;
return;
}
if (e.Column.Index == ColumnSize.Index)
{
VDI vdi1 = ((VDIRow)dataGridViewVDIs.Rows[e.RowIndex1]).VDI;
VDI vdi2 = ((VDIRow)dataGridViewVDIs.Rows[e.RowIndex2]).VDI;
long diff = vdi1.virtual_size - vdi2.virtual_size;
e.SortResult =
diff > 0 ? 1 :
diff < 0 ? -1 :
0;
e.Handled = true;
}
}
private void dataGridViewVDIs_SelectedIndexChanged(object sender, EventArgs e)
{
RefreshButtons();
}
private void dataGridViewVDIs_KeyUp(object sender, KeyEventArgs e)
{
if (e.KeyCode != Keys.Apps)
return;
if (dataGridViewVDIs.SelectedRows.Count == 0)
{
// 3 is the defaul control margin
contextMenuStrip1.Show(dataGridViewVDIs, 3, dataGridViewVDIs.ColumnHeadersHeight + 3);
}
else
{
DataGridViewRow row = dataGridViewVDIs.SelectedRows[0];
contextMenuStrip1.Show(dataGridViewVDIs, 3, row.Height * (row.Index + 2));
}
}
private void dataGridViewVDIs_MouseUp(object sender, MouseEventArgs e)
{
DataGridView.HitTestInfo hitTestInfo = dataGridViewVDIs.HitTest(e.X, e.Y);
if (hitTestInfo.Type == DataGridViewHitTestType.None)
{
dataGridViewVDIs.ClearSelection();
}
else if (hitTestInfo.Type == DataGridViewHitTestType.Cell && e.Button == MouseButtons.Right
&& 0 <= hitTestInfo.RowIndex && hitTestInfo.RowIndex < dataGridViewVDIs.Rows.Count
&& !dataGridViewVDIs.Rows[hitTestInfo.RowIndex].Selected)
{
// Select the row that the user right clicked on (similiar to outlook) if it's not already in the selection
// (avoids clearing a multiselect if you right click inside it)
// Check if the CurrentCell is the cell the user right clicked on (but the row is not Selected) [CA-64954]
// This happens when the grid is initially shown: the current cell is the first cell in the first column, but the row is not selected
if (dataGridViewVDIs.CurrentCell == dataGridViewVDIs[hitTestInfo.ColumnIndex, hitTestInfo.RowIndex])
dataGridViewVDIs.Rows[hitTestInfo.RowIndex].Selected = true;
else
dataGridViewVDIs.CurrentCell = dataGridViewVDIs[hitTestInfo.ColumnIndex, hitTestInfo.RowIndex];
}
if ((hitTestInfo.Type == DataGridViewHitTestType.None || hitTestInfo.Type == DataGridViewHitTestType.Cell)
&& e.Button == MouseButtons.Right)
{
contextMenuStrip1.Show(dataGridViewVDIs, new Point(e.X, e.Y));
}
}
#endregion
#region Button and context menu population
private void contextMenuStrip_Opening(object sender, CancelEventArgs e)
{
bool rescan = buttonRescan.Enabled;
bool add = addVirtualDiskButton.Enabled;
bool move = buttonMove.Enabled;
bool delete = RemoveButton.Enabled;
bool edit = EditButton.Enabled;
if (!(rescan || add || move || delete || edit))
{
e.Cancel = true;
return;
}
rescanToolStripMenuItem.Visible = rescan;
addToolStripMenuItem.Visible = add;
moveVirtualDiskToolStripMenuItem.Visible = move;
deleteVirtualDiskToolStripMenuItem.Visible = delete;
editVirtualDiskToolStripMenuItem.Visible = edit;
toolStripSeparator1.Visible = (rescan || add || move || delete) && edit;
}
private void RefreshButtons()
{
SelectedItemCollection vdis = SelectedVDIs;
// Delete button
// The user can see that this disk is attached to more than one VMs. Allow deletion of multiple VBDs (non default behaviour),
// but don't allow them to be deleted if a running vm is using the disk (default behaviour).
DeleteVirtualDiskCommand deleteCmd = new DeleteVirtualDiskCommand(Program.MainWindow, vdis) {AllowMultipleVBDDelete = true};
if (deleteCmd.CanExecute())
{
RemoveButton.Enabled = true;
RemoveButtonContainer.RemoveAll();
}
else
{
RemoveButton.Enabled = false;
RemoveButtonContainer.SetToolTip(deleteCmd.ToolTipText);
}
// Move button
Command moveCmd = MoveVirtualDiskDialog.MoveMigrateCommand(Program.MainWindow, vdis);
if (moveCmd.CanExecute())
{
buttonMove.Enabled = true;
toolTipContainerMove.RemoveAll();
}
else
{
buttonMove.Enabled = false;
toolTipContainerMove.SetToolTip(moveCmd.ToolTipText);
}
// Rescan button
if (sr == null || sr.Locked)
{
buttonRescan.Enabled = false;
}
else if (HelpersGUI.BeingScanned(sr))
{
buttonRescan.Enabled = false;
toolTipContainerRescan.SetToolTip(Messages.SCAN_IN_PROGRESS_TOOLTIP);
}
else
{
buttonRescan.Enabled = true;
toolTipContainerRescan.RemoveAll();
}
// Add VDI button
addVirtualDiskButton.Enabled = sr != null && !sr.Locked;
// Properties button
if (vdis.Count == 1)
{
VDI vdi = vdis.AsXenObjects<VDI>()[0];
EditButton.Enabled = sr != null && !sr.Locked && !vdi.is_a_snapshot && !vdi.Locked;
}
else
EditButton.Enabled = false;
}
#endregion
#region Actions on Vdis
private void Rescan()
{
SrRefreshAction a = new SrRefreshAction(sr);
a.RunAsync();
}
private void AddVdi()
{
if (sr != null)
Program.MainWindow.ShowPerConnectionWizard(sr.Connection, new NewDiskDialog(sr.Connection, sr));
}
private void MoveSelectedVdis()
{
SelectedItemCollection vdis = SelectedVDIs;
Command cmd = MoveVirtualDiskDialog.MoveMigrateCommand(Program.MainWindow, vdis);
if (cmd.CanExecute())
cmd.Execute();
}
private void RemoveSelectedVdis()
{
SelectedItemCollection vdis = SelectedVDIs;
DeleteVirtualDiskCommand cmd = new DeleteVirtualDiskCommand(Program.MainWindow, vdis) {AllowMultipleVBDDelete = true};
if (cmd.CanExecute())
cmd.Execute();
}
private void EditSelectedVdis()
{
SelectedItemCollection vdis = SelectedVDIs;
if (vdis.Count != 1)
return;
VDI vdi = vdis.AsXenObjects<VDI>()[0];
if (vdi.is_a_snapshot)
return;
new PropertiesDialog(vdi).ShowDialog(this);
}
#endregion
#region Button and ToolStripMenuItem handlers
private void addVirtualDiskButton_Click(object sender, EventArgs e)
{
AddVdi();
}
private void RemoveButton_Click(object sender, EventArgs e)
{
RemoveSelectedVdis();
}
private void EditButton_Click(object sender, EventArgs e)
{
EditSelectedVdis();
}
private void buttonRescan_Click(object sender, EventArgs e)
{
Rescan();
}
private void buttonMove_Click(object sender, EventArgs e)
{
MoveSelectedVdis();
}
private void rescanToolStripMenuItem_Click(object sender, EventArgs e)
{
Rescan();
}
private void addToolStripMenuItem_Click(object sender, EventArgs e)
{
AddVdi();
}
private void editVirtualDiskToolStripMenuItem_Click(object sender, EventArgs e)
{
EditSelectedVdis();
}
private void moveVirtualDiskToolStripMenuItem_Click(object sender, EventArgs e)
{
MoveSelectedVdis();
}
private void deleteVirtualDiskToolStripMenuItem_Click(object sender, EventArgs e)
{
RemoveSelectedVdis();
}
#endregion
public class VDIRow : DataGridViewRow
{
public VDI VDI { get; private set; }
public VDIRow(VDI vdi)
{
VDI = vdi;
for (int i = 0; i < 5; i++)
{
Cells.Add(new DataGridViewTextBoxCell());
Cells[i].Value = GetCellText(i);
}
}
private string GetCellText(int cellIndex)
{
switch (cellIndex)
{
case 0:
return VDI.Name;
case 1:
string name;
return VDI.sm_config.TryGetValue("displayname", out name) ? name : "";
case 2:
return VDI.Description;
case 3:
return VDI.SizeText;
case 4:
return VDI.VMsOfVDI;
default:
return "";
}
}
public void RefreshRowDetails()
{
for (int i = 0; i < 5; i++)
{
Cells[i].Value = GetCellText(i);
}
}
}
public struct VDIsData
{
public List<VDIRow> VdiRowsToUpdate { get; private set; }
public List<VDIRow> VdiRowsToRemove { get; private set; }
public List<VDI> VdisToAdd { get; private set; }
public bool ShowStorageLink { get; private set; }
public VDIsData(List<VDIRow> vdiRowsToUpdate, List<VDIRow> vdiRowsToRemove, List<VDI> vdisToAdd,
bool showStorageLink) : this()
{
VdiRowsToUpdate = vdiRowsToUpdate;
VdiRowsToRemove = vdiRowsToRemove;
VdisToAdd = vdisToAdd;
ShowStorageLink = showStorageLink;
}
}
public class RefreshGridRequest
{
public SR SR { get; private set; }
public bool Reset { get; private set; }
public RefreshGridRequest(SR sr, bool reset)
{
SR = sr;
Reset = reset;
}
}
public class VDIsDataGridViewBuilder
{
private readonly Control owner;
private readonly object _locker = new object();
private BackgroundWorker worker;
private Queue<RefreshGridRequest> queue = new Queue<RefreshGridRequest>();
public VDIsDataGridViewBuilder(Control owner)
{
this.owner = owner;
worker = new BackgroundWorker {WorkerSupportsCancellation = true};
worker.DoWork += DoWork;
worker.RunWorkerCompleted += (RunWorkerCompleted);
}
private void RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (e.Cancelled || e.Error != null)
return;
lock (_locker)
{
if (queue.Count > 0 && !worker.IsBusy)
worker.RunWorkerAsync();
}
}
private VDIsData GetCurrentData(SR sr, IEnumerable<VDIRow> currentVDIRows)
{
List<VDI> vdis =
sr.Connection.ResolveAll(sr.VDIs).Where(
vdi =>
vdi.Show(Properties.Settings.Default.ShowHiddenVMs) &&
!vdi.IsAnIntermediateStorageMotionSnapshot)
.ToList();
bool showStorageLink = vdis.Find(v => v.sm_config.ContainsKey("SVID")) != null;
var vdiRowsToRemove =
currentVDIRows.Where(vdiRow => !vdis.Contains(vdiRow.VDI)).OrderByDescending(row => row.Index).ToList();
var vdiRowsToUpdate = currentVDIRows.Except(vdiRowsToRemove).ToList();
var vdisToAdd = vdis.Except(vdiRowsToUpdate.ConvertAll(vdiRow => vdiRow.VDI)).ToList();
return new VDIsData(vdiRowsToUpdate, vdiRowsToRemove, vdisToAdd, showStorageLink);
}
private void DoWork(object sender, DoWorkEventArgs e)
{
RefreshGridRequest refreshRequest;
lock (_locker)
{
refreshRequest = queue.Count > 0 ? queue.Dequeue() : null;
}
if (worker.CancellationPending)
return;
if (refreshRequest == null || refreshRequest.SR == null)
return;
SrStoragePage page = owner as SrStoragePage;
if (page == null)
return;
if (refreshRequest.Reset)
Program.Invoke(owner, page.dataGridViewVDIs.Rows.Clear);
Program.Invoke(owner, page.RefreshButtons);
IEnumerable<VDIRow> currentVDIRows = Enumerable.Empty<VDIRow>();
Program.Invoke(owner, () => currentVDIRows = page.GetCurrentVDIRows());
VDIsData data = GetCurrentData(refreshRequest.SR, currentVDIRows);
if (worker.CancellationPending)
return;
Program.Invoke(owner, () => ((SrStoragePage)owner).RefreshDataGridView(data));
}
public void AddRequest(RefreshGridRequest refreshGridRequest)
{
lock (_locker)
{
if (refreshGridRequest.Reset)
queue.Clear();
queue.Enqueue(refreshGridRequest);
if (!worker.IsBusy)
worker.RunWorkerAsync();
}
}
public void Stop()
{
if (!worker.CancellationPending)
worker.CancelAsync();
}
}
}
}