i need to clone an entire treeview to implement Undo\Redo actions. i try in different ways but it appears that directly isn't possible. I mean copying the entire treeview, save that copy in a list. Then when the Undo action is requested decrement a specific counter of levels of undo and replace the actual treeview with the treeview that exist in the index positions of the list. if I do redo the same happens but lvls is incremented so i recover what come next. Every time that a modifications occurs in the treeview i need to save that copy and increment the lvl counter
I think you should consider going about this a different way.
You don't need to store the entire TreeView in order to keep track of undos and redos. You only need to keep track of the changes:
If you add a node, the undo is to remove it, so you only need a reference to that node.
If you remove it, the undo is to add it, so you need a reference to the added node and the node it was added to.
If you delete a whole branch, then you need to store the branch and the node it was deleted from.
And so forth. I think you'll have better luck doing it this way than trying to store the entire tree. And I've done it this way in applications where I've implemented undo/redo.
Instead of cloning the treeview itself you could simply clone the TreeNodes (see TreeNode.Clone()). It seems like the important bit is the relationship between the nodes, so this would probably work for you (I say "probably" because I haven't actually done this myself, so I can't tell you if there are any caveats to be wary of).
Consider implementing Command pattern (GoF):
Put your actions logic into classes
which implement common ICommand
{Do(); Undo();} interface.
On each user action you create object
of command requested and initialize
it with context parameters like new
and old filename.
Call Do(), put object into stack of
completed commands.
Each command is supplied with
context, so by calling Undo() it can
reverse changes.
Consider moving files into temporary
folder instead of removals.
Also, for a quick linear undo/redo, another option is the Memento pattern using zip of file as memento.
This is my shot at it, no big objects being moved in memory:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using System.Drawing;
namespace treeundoredo
{
static class Program
{
static TreeNode GetNode(string id, TreeNodeCollection root)
{
foreach (TreeNode node in root)
if (node.Text == id)
return node;
else
{
var subnode = GetNode(id, node.Nodes);
if (subnode != null)
return subnode;
}
return null;
}
[STAThread]
static void Main()
{
// list of (node, parent)
List<KeyValuePair<string, string>> undo = new List<KeyValuePair<string, string>>(), redo = new List<KeyValuePair<string, string>>();
Random rng = new Random();
TreeView Tree = new TreeView() { Size = new Size(200, 250) };
Tree.Nodes.Add("Root");
Tree.NodeMouseClick += (s, e) => { undo.Add(new KeyValuePair<string, string>(e.Node.Nodes.Add(rng.Next().ToString()).Text, e.Node.Text)); Tree.ExpandAll(); redo.Clear(); };
Button Undo = new Button() { Text = "Undo", Left = 205 };
Undo.Click += (s, e) => { if (undo.Count > 0) { var kvp = undo[undo.Count - 1]; GetNode(kvp.Key, Tree.Nodes).Remove(); redo.Add(kvp); undo.Remove(kvp); } };
Button Redo = new Button() { Text = "Redo", Left = 205, Top = 50 };
Redo.Click += (s, e) => { if (redo.Count > 0) { var kvp = redo[redo.Count - 1]; GetNode(kvp.Value, Tree.Nodes).Nodes.Add(kvp.Key); redo.Remove(kvp); undo.Add(kvp); Tree.ExpandAll(); } };
Form frm = new Form();
frm.Controls.AddRange(new Control[] { Tree, Undo, Redo });
Application.Run(frm);
}
}
}
Related
I am implementing a function to add some data to a tree view - MVP (application consists of Word ribbon (VSTO) + C# code base). Below is my code.
private ElementTreeNode LoadElement(Element element, ElementTreeNode parent = null)
{
ElementTreeNode loadElement;
ElementTreeNode node;
//Add a node to the TreeView that represents the element.
loadElement = Add(text: GetElementNodeText(element.Name), parent: parent);
loadElement.Element = element;
//Add a node to the TreeView for conditions
if (element.TSS.Conditions.Count > 0)
{
Add(text: "Conditions", parent: loadElement).Conditions = element.TSS.Conditions;
}
//Add a node to the TreeView control for each image within the element.
foreach (Image image in element.FrameSets.Active.Images)
{
Add(text: GetImageNodeText, parent: loadElement).Image = image;
}
//Call this procedure recursively to add any nested elements.
foreach (Element objElement in element.FrameSets.Active.Elements)
{
LoadElement(element: objElement, parent: loadElement);
}
return loadElement;
}
Add method :
private ElementTreeNode Add(string text, string key = "", ElementTreeNode parent = null)
{
try
{
TreeNode node = null;
//Create a random key if a key is not specified.
if (key.Length == 0)
{
key = "ElementTreeNode_" + GetRandomNumber();
}
//Add a node to the TreeView control.
if (parent == null) //..............point 1
{
node = treeView.Nodes.Add(key: key, text: text);
}
else // ..............point 2
{
node = parent.Node.Nodes.Add(key: key, text: text);
}
//Add a new ElementTreeNode object to this collection. It will have the same key as objNode.
ElementTreeNode objNode = new ElementTreeNode(node);
elementTreeNodesCollection.Add(key: key, value: objNode);
return objNode;
}
catch (Exception ex)
{
MessageBox.Show(ex.Message + Environment.NewLine + "ElementTreeNodes.Add","MyApp,
MessageBoxButtons.OK, MessageBoxIcon.Error);
return null;
}
}
Create the random number :
public long GetRandomNumber()
{
Random rnd = new Random();
return rnd.Next();
}
The issue is, in my test run
this code first add the parent element to the treeview and added into the collection (Dictionary<string, ElementTreeNode> elementTreeNodesCollection). In the next step, a 'conditions' tree node will be added as a sub node under parent element to the tree node. When I try to add it to the elementTreeNodesCollection, it gives below error.
An item with same key has already been added
The biggest issue is, if I put debug points at point 1 or before (no other debug points in whole codebase) within Add method, I am not receiving any errors and items have been added to the tree view and collection well.
But if I add the debug point at or after point 2, I receive the error. Also when there are no break points, then also I got this error.
I tried setting the key as an incrementing number and then the code ran fine. But I need to use a random number here, as the LoadElement method involves with recursion.
Is this issue is related with the random number generating function? or other case?
I am really stuck in here.
Does anybody have an idea what I am doing wrong in here?
Thank you in advance.
I strongly suspect that the problem is your GetRandomNumber() method, which depends on the current time. If you call it multiple times in quick succession, you'll end up getting the same number out of it.
If you wait a bit between calls - e.g. due to being paused at a breakpoint - you'll get different random numbers.
I'd suggest that the simplest option would be to make the number not random at all - keep a counter that you increment each time you need to create a new key. Or you could use Guid.NewGuid() and convert that to a string, or something similar.
If you really want to use Random, create a single instance of Random that you use for all the calls - but be aware that Random isn't thread-safe; if you need to do work across multiple threads, it all becomes a lot more complicated.
Hey guys new to C# and I am trying to setup a GUI, all I want the GUI to do is have a simple file explorer with a CheckedListBox to represent selected files.
I can get the CheckedListBox to show up and click on files but I'm not sure how to continue from here, most tutorials stop here, or go too advanced with tree view and other things that seem unnecessary for what I am trying to do.
Here is my code:
Any help is appreciated and if you guys could point me in the right direction that would be awesome.
EDIT:
To rephrase my question:
I want the user to select files through the CheckedListBox (user input stops here), and for those selected files to be put in a list that my code can manipulate.
Not sure how to accomplish this after my first foreach loop (which adds all files in the selected directory to the CheckedListBox for user selection).
The second foreach loop is an attempt at this, manipulating the files so that they output their filenames after being selected. However no Messagebox shows up and I assume that their is a disconnect between the user selecting files and the codes attempt at manipulating said files.
Second Edit:
I think I figured it out I made a second button and from here it looks like I can manipulate the chosen files however I want.
Currently the code is working the way I would expect it to work.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.IO;
namespace SelectFiles
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
checkedListBox1.CheckOnClick = true;
}
private void button1_Click(object sender, EventArgs e)
{
FolderBrowserDialog fbd = new FolderBrowserDialog();
if (fbd.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
MessageBox.Show(fbd.SelectedPath);
checkedListBox1.Items.Clear();
string[] files = Directory.GetFiles(fbd.SelectedPath);
foreach (string file in files)
{
checkedListBox1.Items.Add(file);
}
}
private void button2_Click_1(object sender, EventArgs e)
{
List<string> list_all_excelfiles = new List<string>();
foreach (string item in checkedListBox1.CheckedItems)
{
list_all_excelfiles.Add(item);
MessageBox.Show(Path.GetFileName(item));
}
}
}
}
First i advice you to assign Value member and Display member for each item.
Display Member will be visible to user
Value Member will we use in code
To do this first create simple custom class
public class Int_String
{
public int _int { get; set; }
public string _string { get; set; }
}
Be careful because get; set; part is important to be there since if not code will not work
Now what you need to do is create list of items with custom class like this
class YourForm : Form
{
List<Int_String> myList = new List<Int_String>(); //Create list of our custom class
public YourForm()
{
PopulateMyList();
}
private void PopulateMyList()
{
//Here read from database or get data somehow and populate our list like this
//I will populate it manually but you do it in foreach loop
myList.Add(new Int_String { _int = 0, _string = "First Item" });
myList.Add(new Int_String { _int = 1, _string = "Second Item" });
myList.Add(new Int_String { _int = 2, _string = "Third Item" });
}
}
After that you need to assign this list to your checkedListBox which you will do like this:
public YourForm()
{
PopulateMyList();
checkedListBox1.DataSource = myList;
checkedListBox1.DisplayMember = "_string";
checkedListBox1.ValueMember = "_int";
}
And now when you can manipulate with checked items like this:
for(int i = 0; i < checkedListBox1.Items.Count; i++)
{
if(checkedListBox1.Items[i].CheckedState == CheckState.Checked)
{
int itemValueMember = (checkedListBox1.Items[i] as Int_String)._int;
int itemDisplayMember = (checkedListBox1.Items[i] as Int_String)._string;
//Use these two vars for whatever you need
}
}
TWO IMPORTANT TIPS:
I am not sure for this one since i am writing all this from head but i think that visual studio will not show you that there is DisplayMember or ValueMember for checkedBox component BUT also it will not show error. Reason is that they have hidden in intentionally for idk what reason but it will work.
You are able to assign Display and Value member to a lot of components in winforms BUT for some reason checkedListBox is specific. It is specific because you MUST first assign DataSource to it and then tell it checkedListBox.DisplayMember = "_string" ...... For new guy you will ask why it is important. Simple answer is create custom list for test and add 10k items inside it and then first declare datasource and after it Display and Value member. Test how long form will need to load (get out of freeze state). After that do everything same but first declare Display and Value member and then assign datasource and test again. I am telling this from head without testing but before when i needed about 5k rows with 1st solution it took me about 30 sec and second < 1 sec. If you want to know more about it google it but for now this is pretty much info for you.
I want to save the current state of List<List<Button>> lloMatrix in an other variable List<List<Button>> lloMatrixCopy, create a Frame (a class i wrote) with it and add it to a list loFrames. lloMatrixCopy, as a property of the Frame, shall not change afterwards. I tried different ways, but my final List only lists equal lloMatrixCopy everytime, all identical to the latest Version of lloMatrix.
So my question is how to make a copy of the current state of lloMatrix without it getting overwritten afterwards as soon as lloMatrix changes.
List<List<Button>> lloMatrixCopy = new List<List<Button>>;
List<List<Button>> lloMatrix = new List<List<Button>>;
List<Frame> loFrames = new List<Frame>;
//...
//lloMatrix gets filled with objects
//...
private void Btn_Click(object sender, RoutedEventArgs e)
{
lloMatrixCopy = lloMatrix;
var oNewFrame = new Frame(lloMatrixCopy);
loFrames.Add(oNewFrame);
}
lloMatrix is getting changed afterwards, but loFrames shall only list it state at that Moment the button got pressed. I guess it's an easy question, but i tried many Things and it just doesn't work. Also sorry for not perfect english. I hope it's understandable.
EDIT: Thank you for fast Responses, but for some reasons
_lloMatrixCopy = _lloMatrixLeds.Select(original => original.ToList()).ToList();
also doesn't solve the Problem. Here the full Btn_Click()-Method
private void Btn_Click(object sender, RoutedEventArgs e)
{
lloMatrixCopy = lloMatrix.Select(original => original.ToList()).ToList();
var oNewFrame = new Frame(lloMatrixCopy);
loFrames.Add(oNewFrame);
//After adding the copy to the list i want to put lloMatrix back in Default
//mode - which means in my case Change the Background Color of every Button to a specific Default
//Color. but the foreach-loop doenst only Change the lloMatrix, but also the
//copy, so that every Matrix saved in loFrames is a Default Matrix
// Globals.LClickedButtons is a list of every Button in lloMatrix which Background-Color
// isn't equal to the Default-Color
foreach (var btn in Globals.LClickedButtons)
{
btn.Background = loLedColorBrushes[0];
}
}
Every Matrix in loFrames still is a default Matrix as soon as the foreach-loop is done.
this will make a deep copy
using System.Windows.Markup;
using System.Xml;
using System.IO;
T CloneXaml(T source){
string xaml = XamlWriter.Save(T);
using (StringReader stringReader = new StringReader(xaml))
using (xmlReader = XmlReader.Create(stringReader))
return (T)XamlReader.Load(xmlReader);
}
lloMatrixCopy = lloMatrix.Select( inner => inner.ToList().Select(CloneXaml)).ToList();
You need to understand that List<T> is a reference type.
There are two kinds of types in C#: reference types and value types.
Variables of reference types store references to their data (objects),
while variables of value types directly contain their data. With
reference types, two variables can reference the same object;
therefore, operations on one variable can affect the object referenced
by the other variable. With value types, each variable has its own
copy of the data, and it is not possible for operations on one
variable to affect the other (except in the case of ref and out
parameter variables, see ref and out parameter modifier).
This
lloMatrixCopy = lloMatrix;
Simply creates a second reference to each of the items in the list so if you modify an item in one of the lists the corresponding item in the other list gets modified too.
You need to duplicate each item in the list and put it into your second list. Something like this will work
lloMatrixCopy = lloMatrix.Select(original => original.ToList()).ToList();
You need to clone each of individual the Button elements. The easiest way to clone a WPF element is to use the XamlWriter.Save and XamlReader.Load methods as suggested here:
How can you clone a WPF object?
The following only creates a copy of the List<Button>:
_lloMatrixCopy = _lloMatrixLeds.Select(original => original.ToList()).ToList();
Both lists will still hold references to the very same Button elements.
So you need to do something like this to clone the actual Button elements:
foreach (var btn in Globals.LClickedButtons)
{
string xaml = System.Windows.Markup.XamlWriter.Save(btn);
using (System.IO.StringReader stringReader = new System.IO.StringReader(xaml))
{
using (System.Xml.XmlReader xmlReader = System.Xml.XmlReader.Create(stringReader))
{
Button newButton = (Button)System.Windows.Markup.XamlReader.Load(xmlReader);
//...
}
}
}
I have a project to which I delegate the function of creating a library of (static)? classes used in all my other projects. It is referenced via solution in these cases.
For instance, I have an extension which creates checkboxes within a given GroupBox's panel, and that works great:
public static void PreencheCheckboxesPanel(this Panel p, List<CheckBox> listaCheckBoxs) {
var count = 0;
listaCheckBoxs.ForEach(
i => {
i.Location = new Point(10, 10 + ((count) * 25)); //"dynamic" and not-so-effective resizing here
i.AutoSize = true;
count++;
});
p.Controls.AddRange(listaCheckBoxs.ToArray());
}}
Problem is, I need to insert a static checkbox on the top of the list, which will receive a method to (un)?check all the checkboxes below.
So my code will become
internal static CheckBox CKB_ancora = new CheckBox(){};
public static void PreencheCheckboxesPanel(this Panel p, List<CheckBox> listaCheckBoxs) {
var count = 0;
if (adicionaAncora) {
CKB_ancora.Text = textoAncora;
CKB_ancora.CheckedChanged += (sender, args) => {
ChecaCheckBoxes(p, CKB_ancora.Checked);
};
listaCheckBoxs.Insert(0, CKB_ancora);
}
listaCheckBoxs.ForEach(
i => {
i.Location = new Point(10, 10 + ((count) * 25)); //"dynamic" and not-so-effective resizing here
i.AutoSize = true;
count++;
});
p.Controls.AddRange(listaCheckBoxs.ToArray());
}}
where ChecaCheckBoxes is another
public static void ChecaCheckBoxes(this Panel b, bool checkStatus = true) {
var listaCheckBoxs = (from Control c in b.Controls where c is CheckBox select (CheckBox)c).ToList();
listaCheckBoxs.ForEach(
i => {
i.Checked = checkStatus;
});
}
and CKB_ancora needs to be a solution-wide recognized object.
The reason? I have another extension named GetSelectedCheckBoxes which will be used to return all the checked ... ah... checkboxes within the groupbox. And, in order to make sure that the "anchor" (I call it like this, since I don't have a name to a (un)?check-all checkbox) won't be returned as well.
If I run this code, it will compile, but... will run accross an InvalidOperationException at Application.SetCompatibleTextRenderingDefault, right at Main(); Apparently, a control cannot be created/instantied before this method is run at mainpoint, which is the exact definition of "static".
Question: Knowing that I NEED a way to keep this particular check solution-wide visible... How do I do it?
Unfortunately, you have not provided a good, minimal, complete code example, and lacking enough context it is very hard to provide good, specific advice.
That said, it seems that somewhere in this static class of yours, you have a field named CKB_ancora, and you are probably initializing it using a field initializer, possibly like this:
private CheckBox CKB_ancora = new Checkbox();
And having done this, you are finding that when the class is initialized (typically on the first time something in the class is accessed at runtime), that happens too soon and an exception is thrown.
Assuming that's correct, then it seems to me that most obvious and simplest "fix" is to initialize the object lazily. For example:
private Lazy<CheckBox> _ckb_ancora =
new Lazy<CheckBox>(() => new CheckBox());
private CheckBox CKB_ancora { get { return _ckb_ancora.Value; } }
That will wrap the object storage in a property, which in turn uses an instance of Lazy<T> to defer initialization until the first time any code actually tries to access it.
Now, that said, I'm not very enamored of your approach here, with a static member that is used in some instantiated object. What if someone using your library wants to use the code with two or more Panel instances? A Control (including a CheckBox) can't be a child of more than one other Control at a time, so having a single static instance of the Control is just not going to work.
IMHO, it would probably be better to instead use some identifying feature such as the Name or Tag property of the CheckBox to handle the control appropriately (e.g. such as filtering it out of enumerations).
For example:
public static void PreencheCheckboxesPanel(this Panel p, List<CheckBox> listaCheckBoxs) {
var count = 0;
if (adicionaAncora) {
CheckBox CKB_ancora = new CheckBox();
CKB_ancora.Text = textoAncora;
CKB_ancora.Name = "CKB_ancora";
CKB_ancora.CheckedChanged += (sender, args) => {
ChecaCheckBoxes(p, CKB_ancora.Checked);
};
listaCheckBoxs.Insert(0, CKB_ancora);
}
listaCheckBoxs.ForEach(
i => {
i.Location = new Point(10, 10 + ((count) * 25)); //"dynamic" and not-so-effective resizing here
i.AutoSize = true;
count++;
});
p.Controls.AddRange(listaCheckBoxs.ToArray());
}}
And, for example:
public static void ChecaCheckBoxes(this Panel b, bool checkStatus = true) {
var listaCheckBoxs = b.Controls
.OfType<CheckBox>().Where(c => c.Name != "CKB_ancora").ToList();
listaCheckBoxs.ForEach(
i => {
i.Checked = checkStatus;
});
}
That way, when you go to retrieve the list of CheckBox controls, the special one you added at the beginning is ignored.
Even with that, I think the code is still pretty fragile. The above would work better, but IMHO it would probably be even better if you weren't using static members for all of this in the first place. I.e. instead you should design some mechanism that allows you to relate an instance of the helper class to the Panel object it's helping with, so that you can in fact initialize and store per-instance information, without running into problems of order of execution, as well as of limitations of use of the code with just a single client.
Without a better code example, I don't see any good way to offer any specific advice along those lines though.
I need to construct a huge treeview from a composite database table with Grouping.
Grouping is, what we see in SQL Server Management Studio Express. After a Database node, some fixed folders are shown (like, Database Diagrams, Tables, Views, Synonyms, Programmability and Security) and children are grouped in those folders.
Up to this point I have used AfterSelect event and handler to achieve this.
But the problem with AfterSelect is, before selecting the node, the viewer is not able to know whether there is any child available. This is because, the expandable plus sign is not visible.
I want to use BeforeExpand. But the problem with BeforeExpand is, it works if the children are already populated. In that case, when I click groups, nothing happens.
How to solve this?
So codes/web-link will be appreciated.
What I usually do is to add a "dummy child node" wherever there may be children that should be loaded in a lazy manner. This will make the parent have the plus sign, and then you can add code to the AfterExpand event where you do the following:
Check if there are are exactly one child, and if that child is the dummy node (you can use the Tag property to identify the dummy node)
If the dummy node is found, launch a search to get the children and add them to the parent node, finish it off by removing the dummy node.
I typically give the dummy node a text like "Loading data. Please wait..." or so, so that the user gets some info on what is going on.
Update
I put together a simple example:
public class TreeViewSample : Form
{
private TreeView _treeView;
public TreeViewSample()
{
this._treeView = new System.Windows.Forms.TreeView();
this._treeView.Location = new System.Drawing.Point(12, 12);
this._treeView.Size = new System.Drawing.Size(200, 400);
this._treeView.AfterExpand +=
new TreeViewEventHandler(TreeView_AfterExpand);
this.ClientSize = new System.Drawing.Size(224, 424);
this.Controls.Add(this._treeView);
this.Text = "TreeView Lazy Load Sample";
InitializeTreeView();
}
void TreeView_AfterExpand(object sender, TreeViewEventArgs e)
{
if (e.Node.Nodes.Count == 1 && e.Node.Nodes[0].Tag == "dummy")
{
// this node has not yet been populated, launch a thread
// to get the data
ThreadPool.QueueUserWorkItem(state =>
{
IEnumerable<SomeClass> childItems = GetData();
// load the data into the tree view (on the UI thread)
_treeView.BeginInvoke((Action)delegate
{
PopulateChildren(e.Node, childItems);
});
});
}
}
private void PopulateChildren(TreeNode parent, IEnumerable<SomeClass> childItems)
{
TreeNode child;
TreeNode dummy;
TreeNode originalDummyItem = parent.Nodes[0];
foreach (var item in childItems)
{
child = new TreeNode(item.Text);
dummy = new TreeNode("Loading. Please wait...");
dummy.Tag = "dummy";
child.Nodes.Add(dummy);
parent.Nodes.Add(child);
}
originalDummyItem.Remove();
}
private IEnumerable<SomeClass> GetData()
{
// simulate that this takes some time
Thread.Sleep(500);
return new List<SomeClass>
{
new SomeClass{Text = "One"},
new SomeClass{Text = "Two"},
new SomeClass{Text = "Three"}
};
}
private void InitializeTreeView()
{
TreeNode rootNode = new TreeNode("Root");
TreeNode dummyNode = new TreeNode("Loading. Please wait...");
dummyNode.Tag = "dummy";
rootNode.Nodes.Add(dummyNode);
_treeView.Nodes.Add(rootNode);
}
}
public class SomeClass
{
public string Text { get; set; }
}
It's standard behaviour for a tree to show a "+" in front of every folder/group, and the plus dissapears when clicked on if it's found to have no children, this saves the expensive "do you have children" check.
Alternatively you can provide this information if you have a cheap way of determining if a node has children. This question provides more information.