#region License Information /* HeuristicLab * Copyright (C) 2002-2017 Heuristic and Evolutionary Algorithms Laboratory (HEAL) * * This file is part of HeuristicLab. * * HeuristicLab 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. * * HeuristicLab 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 HeuristicLab. If not, see . */ #endregion using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Windows.Forms; using HeuristicLab.Collections; using HeuristicLab.Common.Resources; using HeuristicLab.Core; using HeuristicLab.Core.Views; using HeuristicLab.MainForm; using HeuristicLab.MainForm.WindowsForms; namespace HeuristicLab.Clients.Hive.JobManager.Views { [View("Hive Project Selector View")] [Content(typeof(IItemList), true)] public partial class HiveProjectSelector : ItemView, IDisposable { private const int greenFlagImageIndex = 0; private const int redFlagImageIndex = 1; private const int slaveImageIndex = 0; private const int slaveGroupImageIndex = 1; public const string additionalSlavesGroupName = "Additional Slaves"; public const string additionalSlavesGroupDescription = "Contains additional slaves which are either ungrouped or the parenting slave group is not assigned to the selected project."; private readonly HashSet mainTreeNodes = new HashSet(); private readonly HashSet filteredTreeNodes = new HashSet(); private readonly HashSet nodeStore = new HashSet(); private readonly HashSet availableResources = new HashSet(); private readonly HashSet assignedResources = new HashSet(); private readonly HashSet includedResources = new HashSet(); private readonly HashSet newAssignedResources = new HashSet(); private readonly HashSet newIncludedResources = new HashSet(); private readonly Dictionary> projectAncestors = new Dictionary>(); private readonly Dictionary> projectDescendants = new Dictionary>(); private readonly Dictionary> resourceAncestors = new Dictionary>(); private readonly Dictionary> resourceDescendants = new Dictionary>(); private IEnumerable addedResources; private IEnumerable removedResources; private IEnumerable addedIncludes; private IEnumerable removedIncludes; private readonly Color addedAssignmentColor = Color.FromArgb(255, 87, 191, 193); // #57bfc1 private readonly Color removedAssignmentColor = Color.FromArgb(255, 236, 159, 72); // #ec9f48 private readonly Color addedIncludeColor = Color.FromArgb(25, 169, 221, 221); // #a9dddd private readonly Color removedIncludeColor = Color.FromArgb(25, 249, 210, 145); // #f9d291 private readonly Color selectedColor = Color.FromArgb(255, 240, 194, 59); // #f0c23b private string currentSearchString; private void resetHiveResourceSelector() { lastSelectedProject = null; selectedProject = null; projectId = null; } private Guid jobId; public Guid JobId { get { return jobId; } set { if (jobId == value) return; jobId = value; resetHiveResourceSelector(); } } private Guid? projectId; public Guid? ProjectId { get { return projectId; } set { if (projectId == value) return; projectId = value; } } private Guid? selectedProjectId; public Guid? SelectedProjectId { get { return selectedProjectId; } set { if (selectedProjectId == value) return; selectedProjectId = value; } } private IEnumerable selectedResourceIds; public IEnumerable SelectedResourceIds { get { return selectedResourceIds; } set { if (selectedResourceIds == value) return; selectedResourceIds = value; } } public bool ChangedProjectSelection { get { if ((lastSelectedProject == null && selectedProject != null) || (lastSelectedProject != null && selectedProject == null) || (lastSelectedProject != null && selectedProject != null && lastSelectedProject.Id != selectedProject.Id)) return true; else return false; } } public bool ChangedResources { get { return !assignedResources.SetEquals(newAssignedResources); } } private Project lastSelectedProject; private Project selectedProject; public Project SelectedProject { get { return selectedProject; } set { lastSelectedProject = selectedProject; if (selectedProject == value) return; selectedProject = value; UpdateResourceTree(); ExtractStatistics(); OnSelectedProjectChanged(); } } public IEnumerable AssignedResources { get { return newAssignedResources; } set { if (newAssignedResources == value) return; newAssignedResources.Clear(); foreach(var resource in value) { newAssignedResources.Add(resource); } } } public new IItemList Content { get { return (IItemList)base.Content; } set { base.Content = value; } } public HiveProjectSelector() { InitializeComponent(); projectsImageList.Images.Add(VSImageLibrary.FlagGreen); projectsImageList.Images.Add(VSImageLibrary.FlagRed); resourcesImageList.Images.Add(VSImageLibrary.MonitorLarge); resourcesImageList.Images.Add(VSImageLibrary.NetworkCenterLarge); } #region Overrides protected override void OnContentChanged() { base.OnContentChanged(); if (Content != null) { UpdateProjectGenealogy(); UpdateResourceGenealogy(); if (SelectedProjectId.HasValue && SelectedProjectId.Value != Guid.Empty) { SelectedProject = GetSelectedProjectById(SelectedProjectId.Value); } else { SelectedProject = null; } //ExtractStatistics(); UpdateProjectTree(); } else { lastSelectedProject = null; selectedProject = null; selectedProjectId = null; projectsTreeView.Nodes.Clear(); resourcesTreeView.Nodes.Clear(); } } #endregion #region Event Handlers private void HiveProjectSelector_Load(object sender, EventArgs e) { projectsTreeView.Nodes.Clear(); resourcesTreeView.Nodes.Clear(); } private void searchTextBox_TextChanged(object sender, EventArgs e) { currentSearchString = searchTextBox.Text.ToLower(); //UpdateFilteredTree(); UpdateProjectTree(); } private void projectsTreeView_MouseDoubleClick(object sender, MouseEventArgs e) { OnProjectsTreeViewDoubleClicked(); } private void projectsTreeView_AfterSelect(object sender, TreeViewEventArgs e) { var node = (Project)e.Node.Tag; if (node == null) { projectsTreeView.SelectedNode = null; } else { ReColorTreeNodes(projectsTreeView.Nodes, selectedColor, Color.Transparent, true); e.Node.BackColor = selectedColor; if(node.Id == projectId) { e.Node.Text += " [current selection]"; } else if(projectId == null || projectId == Guid.Empty) { e.Node.Text += " [new selection]"; } else { e.Node.Text += " [changed selection]"; } } SelectedProject = node; //ExtractStatistics(); } private void resourcesTreeView_MouseDown(object sender, MouseEventArgs e) { var node = resourcesTreeView.GetNodeAt(new Point(e.X, e.Y)); if (node == null) { resourcesTreeView.SelectedNode = null; } ExtractStatistics((Resource)node?.Tag); } private void resourcesTreeView_BeforeCheck(object sender, TreeViewCancelEventArgs e) { var checkedResource = (Resource)e.Node.Tag; if (newIncludedResources.Contains(checkedResource) || checkedResource.Id == Guid.Empty) e.Cancel = true; } private void resourcesTreeView_AfterCheck(object sender, TreeViewEventArgs e) { var checkedResource = (Resource)e.Node.Tag; if (e.Node.Checked) { newAssignedResources.Add(checkedResource); } else { newAssignedResources.Remove(checkedResource); } UpdateNewResourceTree(); ExtractStatistics(); //ExtractStatistics((Resource)resourcesTreeView.SelectedNode?.Tag); OnAssignedResourcesChanged(); } private void resourcesTreeView_NodeMouseDoubleClick(object sender, TreeNodeMouseClickEventArgs e) { return; } #endregion #region Helpers #region old private void UpdateMainTree() { mainTreeNodes.Clear(); foreach (Project g in Content.OrderBy(x => x.Name)) { if (g.ParentProjectId == null) { TreeNode tn = new TreeNode(); tn.ImageIndex = greenFlagImageIndex; tn.SelectedImageIndex = tn.ImageIndex; tn.Tag = g; tn.Text = g.Name; tn.Checked = assignedResources.Any(x => x.Id == g.Id); BuildMainTree(tn); mainTreeNodes.Add(tn); } } UpdateFilteredTree(); } private void BuildMainTree(TreeNode tn) { foreach (Project r in Content.Where(s => s.ParentProjectId != null && s.ParentProjectId == ((Project)tn.Tag).Id).OrderBy(x => x.Name)) { TreeNode stn = new TreeNode(r.Name); stn.ImageIndex = redFlagImageIndex; stn.SelectedImageIndex = stn.ImageIndex; stn.Tag = r; stn.Checked = assignedResources.Any(x => x.Id == r.Id); tn.Nodes.Add(stn); mainTreeNodes.Add(stn); BuildMainTree(stn); } } private void UpdateFilteredTree() { filteredTreeNodes.Clear(); foreach (TreeNode n in mainTreeNodes) { n.BackColor = SystemColors.Window; if (currentSearchString == null || ((Project)n.Tag).Name.ToLower().Contains(currentSearchString)) { n.BackColor = string.IsNullOrEmpty(currentSearchString) ? SystemColors.Window : Color.LightBlue; filteredTreeNodes.Add(n); TraverseParentNodes(n); } } UpdateProjectsTree(); } private void UpdateProjectsTree() { projectsTreeView.Nodes.Clear(); nodeStore.Clear(); foreach (TreeNode node in filteredTreeNodes) { var clone = nodeStore.SingleOrDefault(x => ((Project)x.Tag).Id == ((Project)node.Tag).Id); if (clone == null) { clone = (TreeNode)node.Clone(); nodeStore.Add(clone); clone.Nodes.Clear(); } foreach (TreeNode child in node.Nodes) if (filteredTreeNodes.Any(x => ((Project)x.Tag).Id == ((Project)child.Tag).Id)) { var childClone = nodeStore.SingleOrDefault(x => ((Project)x.Tag).Id == ((Project)child.Tag).Id); if (childClone == null) { childClone = (TreeNode)child.Clone(); nodeStore.Add(childClone); childClone.Nodes.Clear(); } clone.Nodes.Add(childClone); } } projectsTreeView.Nodes.AddRange(nodeStore.Where(x => ((Project)x.Tag).ParentProjectId == null).ToArray()); if (string.IsNullOrEmpty(currentSearchString)) ExpandSlaveGroupNodes(); else projectsTreeView.ExpandAll(); } private void TraverseParentNodes(TreeNode node) { if (node != null) { for (TreeNode parent = node.Parent; parent != null; parent = parent.Parent) filteredTreeNodes.Add(parent); } } #endregion private Project GetSelectedProjectById(Guid projectId) { return Content.Where(x => x.Id == projectId).SingleOrDefault(); } private void UpdateProjectTree() { if (string.IsNullOrEmpty(currentSearchString)) { BuildProjectTree(Content); } else { HashSet filteredProjects = new HashSet(); foreach(var project in Content) { if(project.Name.ToLower().Contains(currentSearchString.ToLower())) { filteredProjects.Add(project); filteredProjects.UnionWith(projectAncestors[project.Id]); } } BuildProjectTree(filteredProjects); } } private void BuildProjectTree(IEnumerable projects) { projectsTreeView.Nodes.Clear(); if (!projects.Any()) return; // select all top level projects (withouth parent, or without parent within current project collection) var mainProjects = new HashSet(projects.Where(x => x.ParentProjectId == null)); var parentedMainProjects = new HashSet(projects .Where(x => x.ParentProjectId.HasValue && !projects.Select(y => y.Id).Contains(x.ParentProjectId.Value))); mainProjects.UnionWith(parentedMainProjects); var subProbjects = new HashSet(projects.Except(mainProjects)); var stack = new Stack(mainProjects.OrderByDescending(x => x.Name)); TreeNode currentNode = null; Project currentProject = null; while(stack.Any()) { var newProject = stack.Pop(); var newNode = new TreeNode(newProject.Name) { Tag = newProject }; while (currentNode != null && newProject.ParentProjectId != currentProject.Id) { currentNode = currentNode.Parent; currentProject = currentNode == null ? null : (Project)currentNode.Tag; } if (currentNode == null) { projectsTreeView.Nodes.Add(newNode); newNode.ImageIndex = greenFlagImageIndex; } else { currentNode.Nodes.Add(newNode); newNode.ImageIndex = redFlagImageIndex; } newNode.SelectedImageIndex = newNode.ImageIndex; if (SelectedProject != null && SelectedProject.Id.Equals(newProject.Id)) { newNode.BackColor = selectedColor; if(SelectedProject.Id == projectId) { newNode.Text += " [current selection]"; } else if (projectId == null || projectId == Guid.Empty) { newNode.Text += " [new selection]"; } else { newNode.Text += " [changed selection]"; } } if (!string.IsNullOrEmpty(currentSearchString) && newProject.Name.ToLower().Contains(currentSearchString.ToLower())) { newNode.BackColor = Color.LightBlue; } var childProjects = subProbjects.Where(x => x.ParentProjectId == newProject.Id); if (childProjects.Any()) { foreach (var project in childProjects.OrderByDescending(x => x.Name)) { subProbjects.Remove(project); stack.Push(project); } currentNode = newNode; currentProject = newProject; } } projectsTreeView.ExpandAll(); } private void UpdateProjectGenealogy() { projectAncestors.Clear(); projectDescendants.Clear(); var projects = Content; foreach (var p in projects) { projectAncestors.Add(p.Id, new HashSet()); projectDescendants.Add(p.Id, new HashSet()); } foreach (var p in projects) { var parentProjectId = p.ParentProjectId; while (parentProjectId != null) { var parent = projects.SingleOrDefault(x => x.Id == parentProjectId); if (parent != null) { projectAncestors[p.Id].Add(parent); projectDescendants[parent.Id].Add(p); parentProjectId = parent.ParentProjectId; } else { parentProjectId = null; } } } } private static IEnumerable GetAssignedResourcesForProject(Guid projectId) { var assignedProjectResources = HiveServiceLocator.Instance.CallHiveService(s => s.GetAssignedResourcesForProject(projectId)); return HiveClient.Instance.Resources.Where(x => assignedProjectResources.Select(y => y.ResourceId).Contains(x.Id)); } private static IEnumerable GetAssignedResourcesForJob(Guid jobId) { var assignedJobResources = HiveServiceLocator.Instance.CallHiveService(s => s.GetAssignedResourcesForJob(jobId)); return HiveClient.Instance.Resources.Where(x => assignedJobResources.Select(y => y.ResourceId).Contains(x.Id)); } private void UpdateResourceTree() { UpdateAvailableResources(); UpdateAssignedResources(); UpdateIncludedResources(); BuildResourceTree(availableResources); } private void UpdateNewResourceTree() { UpdateNewAssignedResources(); UpdateNewIncludedResources(); BuildResourceTree(availableResources); } private void UpdateAvailableResources() { availableResources.Clear(); if (selectedProject != null) { var assignedProjectResources = GetAssignedResourcesForProject(selectedProject.Id); foreach (var resource in assignedProjectResources) { availableResources.Add(resource); foreach(var descendant in resourceDescendants[resource.Id]) { availableResources.Add(descendant); } } } //ExtractStatistics(); //OnAssignedResourcesChanged(); } private void UpdateAssignedResources() { assignedResources.Clear(); newAssignedResources.Clear(); if (JobId == Guid.Empty || JobId == null) { // new, unchanged jobs get all avaialable resources // update new assigned resources if(selectedResourceIds == null) { foreach (var resource in availableResources .Where(x => !x.ParentResourceId.HasValue || !availableResources.Select(y => y.Id).Contains(x.ParentResourceId.Value))) { newAssignedResources.Add(resource); } } else { foreach(var resource in availableResources.Where(x => selectedResourceIds.Contains(x.Id))) { newAssignedResources.Add(resource); } } } else { // existent, unchanged jobs get all assigned resources // update assigned resources var assignedJobResources = GetAssignedResourcesForJob(JobId); foreach (var resource in assignedJobResources) { assignedResources.Add(resource); if (selectedResourceIds == null) { newAssignedResources.Add(resource); } } if(selectedResourceIds != null) { foreach (var resource in availableResources.Where(x => selectedResourceIds.Contains(x.Id))) { newAssignedResources.Add(resource); } } } //ExtractStatistics(); OnAssignedResourcesChanged(); } private void UpdateNewAssignedResources() { for(int i = newAssignedResources.Count-1; i>=0; i--) { if(newAssignedResources.Intersect(resourceAncestors[newAssignedResources.ElementAt(i).Id]).Any()) { newAssignedResources.Remove(newAssignedResources.ElementAt(i)); } } } private void UpdateIncludedResources() { includedResources.Clear(); newIncludedResources.Clear(); if (JobId != Guid.Empty) { foreach (var item in assignedResources) { foreach (var descendant in resourceDescendants[item.Id]) { includedResources.Add(descendant); } } } foreach (var item in newAssignedResources) { foreach (var descendant in resourceDescendants[item.Id]) { newIncludedResources.Add(descendant); } } } private void UpdateNewIncludedResources() { newIncludedResources.Clear(); foreach (var item in newAssignedResources) { foreach (var descendant in resourceDescendants[item.Id]) { newIncludedResources.Add(descendant); } } } private void UpdateResourceGenealogy() { resourceAncestors.Clear(); resourceDescendants.Clear(); var resources = HiveClient.Instance.Resources; foreach (var r in resources) { resourceAncestors.Add(r.Id, new HashSet()); resourceDescendants.Add(r.Id, new HashSet()); } foreach (var r in resources) { var parentResourceId = r.ParentResourceId; while (parentResourceId != null) { var parent = resources.SingleOrDefault(x => x.Id == parentResourceId); if (parent != null) { resourceAncestors[r.Id].Add(parent); resourceDescendants[parent.Id].Add(r); parentResourceId = parent.ParentResourceId; } else { parentResourceId = null; } } } } private void BuildResourceTree(IEnumerable resources) { resourcesTreeView.Nodes.Clear(); if (!resources.Any()) return; resourcesTreeView.BeforeCheck -= resourcesTreeView_BeforeCheck; resourcesTreeView.AfterCheck -= resourcesTreeView_AfterCheck; var mainResources = new HashSet(resources.OfType().Where(x => x.ParentResourceId == null)); var parentedMainResources = new HashSet(resources.OfType() .Where(x => x.ParentResourceId.HasValue && !resources.Select(y => y.Id).Contains(x.ParentResourceId.Value))); mainResources.UnionWith(parentedMainResources); var subResources = new HashSet(resources.Except(mainResources)); var addedAssignments = newAssignedResources.Except(assignedResources); var removedAssignments = assignedResources.Except(newAssignedResources); var addedIncludes = newIncludedResources.Except(includedResources); var removedIncludes = includedResources.Except(newIncludedResources); HashSet expandedNodes = new HashSet(); TreeNode currentNode = null; Resource currentResource = null; var stack = new Stack(mainResources.OrderByDescending(x => x.Name)); while (stack.Any()) { var newResource = stack.Pop(); var newNode = new TreeNode(newResource.Name) { Tag = newResource }; // search for parent node of newNode and save in currentNode // necessary since newNodes (stack top items) might be siblings // or grand..grandparents of previous node (currentNode) while (currentNode != null && newResource.ParentResourceId != currentResource.Id) { currentNode = currentNode.Parent; currentResource = currentNode == null ? null : (Resource)currentNode.Tag; } if (currentNode == null) { resourcesTreeView.Nodes.Add(newNode); } else { currentNode.Nodes.Add(newNode); } if(newAssignedResources.Select(x => x.Id).Contains(newResource.Id) || assignedResources.Select(x => x.Id).Contains(newResource.Id) || newIncludedResources.Select(x => x.Id).Contains(newResource.Id) || includedResources.Select(x => x.Id).Contains(newResource.Id)) { expandedNodes.Add(newNode); } if (newAssignedResources.Select(x => x.Id).Contains(newResource.Id)) { newNode.Checked = true; } else if (newIncludedResources.Select(x => x.Id).Contains(newResource.Id)) { newNode.Checked = true; newNode.ForeColor = SystemColors.GrayText; } if (includedResources.Select(x => x.Id).Contains(newResource.Id) && newIncludedResources.Select(x => x.Id).Contains(newResource.Id)) { newNode.Text += " [included]"; } else if (addedIncludes.Select(x => x.Id).Contains(newResource.Id)) { newNode.BackColor = addedIncludeColor; newNode.ForeColor = SystemColors.GrayText; newNode.Text += " [added include]"; } else if (removedIncludes.Select(x => x.Id).Contains(newResource.Id)) { newNode.BackColor = removedIncludeColor; newNode.Text += " [removed include]"; } if (addedAssignments.Select(x => x.Id).Contains(newResource.Id)) { newNode.BackColor = addedAssignmentColor; newNode.ForeColor = SystemColors.ControlText; newNode.Text += " [added selection]"; } else if (removedAssignments.Select(x => x.Id).Contains(newResource.Id)) { newNode.BackColor = removedAssignmentColor; newNode.ForeColor = SystemColors.ControlText; newNode.Text += " [removed selection]"; } if (newResource is Slave) { newNode.ImageIndex = slaveImageIndex; } else { newNode.ImageIndex = slaveGroupImageIndex; var childResources = subResources.Where(x => x.ParentResourceId == newResource.Id); if (childResources.Any()) { foreach (var resource in childResources.OrderByDescending(x => x.Name)) { subResources.Remove(resource); stack.Push(resource); } currentNode = newNode; currentResource = newResource; } } newNode.SelectedImageIndex = newNode.ImageIndex; } var singleSlaves = subResources.OfType(); if (singleSlaves.Any()) { var additionalNode = new TreeNode(additionalSlavesGroupName) { ForeColor = SystemColors.GrayText, Tag = new SlaveGroup() { Name = additionalSlavesGroupName, Description = additionalSlavesGroupDescription } }; foreach (var slave in singleSlaves.OrderBy(x => x.Name)) { var slaveNode = new TreeNode(slave.Name) { Tag = slave }; additionalNode.Nodes.Add(slaveNode); } resourcesTreeView.Nodes.Add(additionalNode); } foreach (var node in expandedNodes) { node.Expand(); var parent = node.Parent; while(parent != null) { parent.Expand(); parent = parent.Parent; } } resourcesTreeView.BeforeCheck += resourcesTreeView_BeforeCheck; resourcesTreeView.AfterCheck += resourcesTreeView_AfterCheck; //resourcesTreeView.ExpandAll(); } private void ExpandSlaveGroupNodes() { foreach (TreeNode n in nodeStore.Where(x => x.Tag is SlaveGroup)) { TreeNode[] children = new TreeNode[n.Nodes.Count]; n.Nodes.CopyTo(children, 0); if (children.Any(x => x.Tag is SlaveGroup)) n.Expand(); } } private void ExtractStatistics(Resource resource = null) { HashSet newAssignedSlaves = new HashSet(newAssignedResources.OfType()); foreach (var slaveGroup in newAssignedResources.OfType()) { foreach(var slave in resourceDescendants[slaveGroup.Id].OfType()) { newAssignedSlaves.Add(slave); } } HashSet selectedSlaves = null; if (resource != null) { var slaveGroup = resource as SlaveGroup; if (slaveGroup != null) { selectedSlaves = new HashSet(resourceDescendants[slaveGroup.Id].OfType()); //selectedSlaves.IntersectWith(newAssignedSlaves); } else { selectedSlaves = new HashSet(new[] { resource as Slave }); } } else { selectedSlaves = newAssignedSlaves; } int sumCores = selectedSlaves.Sum(x => x.Cores.GetValueOrDefault()); int sumFreeCores = selectedSlaves.Sum(x => x.FreeCores.GetValueOrDefault()); double sumMemory = selectedSlaves.Sum(x => x.Memory.GetValueOrDefault()) / 1024.0; double sumFreeMemory = selectedSlaves.Sum(x => x.FreeMemory.GetValueOrDefault()) / 1024.0; coresSummaryLabel.Text = $"{sumCores} Total ({sumFreeCores} Free / {sumCores - sumFreeCores} Used)"; memorySummaryLabel.Text = $"{sumMemory:0.00} GB Total ({sumFreeMemory:0.00} GB Free / {(sumMemory - sumFreeMemory):0.00} GB Used)"; } private void ReColorTreeNodes(TreeNodeCollection nodes, Color c1, Color c2, bool resetText) { foreach (TreeNode n in nodes) { if (n.BackColor.Equals(c1)) { n.BackColor = c2; if(resetText) n.Text = ((Project)n.Tag).Name; } if (n.Nodes.Count > 0) { ReColorTreeNodes(n.Nodes, c1, c2, resetText); } } } #endregion #region Events public event EventHandler SelectedProjectChanged; private void OnSelectedProjectChanged() { SelectedProjectChanged?.Invoke(this, EventArgs.Empty); } public event EventHandler AssignedResourcesChanged; private void OnAssignedResourcesChanged() { AssignedResourcesChanged?.Invoke(this, EventArgs.Empty); } public event EventHandler ProjectsTreeViewDoubleClicked; private void OnProjectsTreeViewDoubleClicked() { ProjectsTreeViewDoubleClicked?.Invoke(this, EventArgs.Empty); } #endregion } }