I am building a treeview with a list of ScanItem. The class of ScanItem is actually:
public class ScanItem
{
public string FullPath { get; set; }
public string Name
{
get
{
return Path.GetFileName(FullPath);
}
}
public DateTime ModifiedDate { get; set; }
public DateTime CreatedDate { get; set; }
public FileAttributes Attributes { get; set; }
public bool IsDirectory { get; set; }
public string Extension
{
get
{
if (IsDirectory)
return "Folder";
else
return Path.GetExtension(Name);
}
}
public UInt64 Size { get; set; }
}
In order for me to create a treeview I needed to create two other classes in order to distinguish the folders and files in my treeview:
public class ScanFile : ScanItem
{
}
public class ScanDir : ScanItem
{
public List<ScanItem> Items { get; set; }
public ScanDir()
{
Items = new List<ScanItem>();
}
}
Note that the class ScanFile is just like the ScanItem and the ScanDir class has an extra property called Items and will contain a list of items of itself.
So if I where to iterate through this direcotory (C:\Temp):
my List will actually contain:
note that if I expand one ScanDir object I will get another List:
in order to populate the following treeview:
So I was able to populate this list using recursion by searching for files and directories in a specific path.
I just wanted to explain my situation because there are several places in the internet that enable you to filter a treeview and that is what I actually want to do. But it will be nice if I can iterate through each item in List and then remove it if some criteria is not met:
I have actually tried using the following recursive method to filter my results.
public List<ScanItem> search(List<ScanItem> items)
{
var filter = new List<ScanItem>();
foreach (var item in items)
{
if (!item.FullPath.Contains("stringIwantToLookFor")) continue;
filter.Add(item);
if (item.IsDirectory)
{
search(((ScanDir)item).Items);
}
}
return filter;
}
I think that if an item is found I need to add all the parent root directories and that's why it does not work. The reason why I want to build my own recursion method is because I want to be able to filter the treeview based on spesific criteria.
EDIT:
In other words if I want to have all the items that contain "X.txt" in my listview I want to just see:
I would do it like this: create public abstract ScanItem Seach(string s) on your ScanItem. You can then call it with the string you want to search for.
The actual implementation would look like this:
ScanFile:
public override ScanItem Seach(string s)
{
if (Name.Contains(s))
return this;
return null;
}
ScanDir:
public override ScanItem Seach(string s)
{
var results = Items.Select(i => i.Seach(s)).Where(i => i != null).ToList();
if (results.Any())
{
var result = (ScanDir)MemberwiseClone();
result.Items = results;
return result;
}
return null;
}
The implementation in ScanFile is easy: if the file matches, return it, else return null. In ScanDir, call Search on all child items recursively. If any of them returned non-null, create a copy of the current object and set the Items of the copy only to those that matched. If none matched, return null.
Note that this will search only through the names of files, not directories. But if you want to do that, such modification is going to be straight-forward.
You should treat the directories a little different because now, if the root directory does not meet the criteria the routine will exit immediately.
Try this: change your ScanItem a little:
public class ScanItem {
...
public virtual bool IsDirectory { get; }
...
}
add this to your scanFile:
public class ScanFile : ScanItem {
public override bool IsDirectory {
get { return false; }
}
}
and this to your scanDir:
public class ScanDir : ScanItem {
public List<ScanItem> Items { get; set; }
public ScanDir() {
Items = new List<ScanItem>();
}
public ScanDir CopyWithoutChildren() {
return new ScanDir() {
FullPath = this.FullPath,
ModifiedDate = this.ModifiedDate,
CreatedDate = this.CreatedDate,
Attributes = this.Attributes,
Size = this.Size
};
}
public override bool IsDirectory {
get { return true; }
}
}
Now do the filtering on the files, omitting empty directories:
public List<ScanItem> search(List<ScanItem> items) {
var filter = new List<ScanItem>();
foreach(var item in items) {
if(item.IsDirectory) {
List<ScanItem> potential = search(((ScanDir)item).Items);
if(potential.Count > 0) {
ScanDir dir = ((ScanDir)item).CopyWithoutChildren();
dir.Items.AddRange(potential);
filter.Add(dir);
}
} else {
if(!item.FullPath.Contains("stringIwantToLookFor")) continue;
filter.Add(item);
}
}
return filter;
}
I didn't test it, but I guess that should do what you want.
I realized my comment to your post might not have been descriptive enough, so I've written some C#-ish pseudocode to demonstrate what I was getting at.
Here's an example of using the Visitor pattern to implement search in a polymorphic, loosely-coupled way:
interface FilesystemVistor
{
void Visit (FilesystemItem item);
}
interface FilesystemItem
{
void Accept(FilesystemVistor visitor);
string Name;
}
class Directory : FilesystemItem
{
private FilesystemItem[] _children;
public void Accept(FilesystemVistor visitor) {
visitor.Visit(this);
foreach(FilesystemItem item in _children)
{
visitor.Visit(item);
}
}
}
class File : FilesystemItem
{
public void Accept(FilesystemVistor visitor) {
visitor.Visit(this);
}
}
class FilesystemSearcher : FilesystemVistor
{
private List<string> _results;
public void Visit(FilesystemItem item) {
if (item.Name == "Foo") { _results.Add(item.Name); }
}
}
This "visitor pattern"-based design will allow you to implement any kind of search without having the search algorithm having to "know" anything about the structure of the file system and the file system doesn't need an extra property like "IsDirectory" to expose its implementation details.
so If I am looking for the files that contain foo this method will populate the files that contain foo in the list 'newList' . I would have to set that list equal to a new list before calling that method. I am obviously missing basic implementation such as changing foo for a parameter etc. I am also missing to remove the empty directories I am working on that.
private List<ScanDir> history = new List<ScanDir>();
private ScanDir LastDir;
private List<ScanItem> newList = new List<ScanItem>();
public void Search(List<ScanItem> allItems) //adds files that contain foo
{
bool updateLastDir = false;
foreach(ScanItem s in allItems)
{
if (updateLastDir)
{
history = (from a in history
select a).Distinct().ToList();
LastDir = null;
for (int i = history.Count - 1; i >= 0; i--)
{
if (history[i].FullPath == Directory.GetParent(s.FullPath).ToString())
{
LastDir = history[i];
break;
}
}
updateLastDir = false;
}
if (s.IsDirectory)
{
var temp = new ScanDir { FullPath = s.FullPath, IsDirectory = true, comparePath = s.comparePath, Attributes = s.Attributes };
if (LastDir == null)
{
newList.Add(temp);
}
else
{
LastDir.Items.Add(temp);
}
LastDir = temp;
history.Add(LastDir);
Search(((ScanDir)s).Items);
history.RemoveAt(history.Count - 1);
updateLastDir = true;
}
else
{
if (s.Name.Contains("Foo")) // then add it
{
if (LastDir == null)
newList.Add(s);
else
LastDir.Items.Add(s);
}
}
}
}
Related
I would like to set a property called "Current" which give me the opportunity to keep track of which item I'm working with. Here's my class Volume:
namespace ConsoleApp3 {
class Volume : Interface
{
public string Name { get; set; }
public Volume()
{
}
}
My interface called: "Interface"
namespace ConsoleApp3{
class Interface
{
public string Name { get; set; }
public int Id { get; set; }
public bool IsCurrent { get; set; }
}
And my RootList Class:
namespace ConsoleApp3{
class RootList<T> : List<T> where T : Interface, new()
{
private int _index;
private int _id;
public RootList()
{
}
public T First
{
get => this[0];
set => this[0] = value;
}
public T Last
{
get => this[this.Count - 1];
set => this[this.Count - 1] = value;
}
public T Current
{
get
{
return this.FirstOrDefault(tTemp => tTemp.IsCurrent == true);
}
set
{
this.RmCurrent();
int _index = this.IndexOf(value);
this[_index].IsCurrent = true;
}
}
public T this[string name]
{
get
{
return this.FirstOrDefault(tTemp => tTemp.Name == name);
}
}
private void RmCurrent()
{
var _iscurrent = this.Where(v => v.IsCurrent = true);
foreach (var item in _iscurrent)
{
item.IsCurrent = false;
}
}
public void Add()
{
this.Add(new T());
this.RmCurrent();
this.Last.IsCurrent = true;
}
}
}
I'm using the "IsCurrent" property to track my items: my item has the "Current" state if the "IsCurrent" property is "true". Only one item can me the "Current" one at the same time.
My "Current" method doesn't work. For exmaple:
VolumeList.Add();
VolumeList.Last.Name = "test_0";
VolumeList.Add();
VolumeList.Last.Name = "test_1";
VolumeList.Add();
VolumeList.Last.Name = "test_2";
In this case, my Current item is the Last one "test_2".
If I do that:
VolumeList.Current = VolumeList[1];
I have two Current items VolumeList[0] and VolumeList[1]. Not only VolumeList[1].
As you can see, I also have a string indexer in my RootList so it has to work with int indexer and string indexer.
Do you have any ideas?
Thanks a lot.
Best regards,
I would not search for an item which is current in private void RmCurrent()
I would only do
private void RmCurrent()
{
foreach (var item in this)
{
item.IsCurrent = false;
}
}
I'd make the RootList track current:
public class RootList<T> : List<T> where T : Interface, new()
{
public T Current { get; private set; }
public T First
{
get => Current = this[0];
set => Current = this[0] = value;
}
public T Last
{
get => Current = this[this.Count - 1];
set => Current = this[this.Count - 1] = value;
}
public T this[string name]
{
//what if there is no such name? what do you want current to be?
//if it should not change, break this lambda up using a temp var
//that you do not assign to current if it is null
get => Current = this.FirstOrDefault(tTemp => tTemp.Name == name);
}
public void Add() //careful, the List<T> you inherit from also has an Add(T), and so calling myList.Add(something) it won't set the current item
{
Current = new T();
this.Add(Current);
}
}
So what's going on ?
Assignments in C# return, as a value, the value that was assigned. It means that this:
myString = "hello";
returns "hello". It can be used in another assignment:
myString2 = (myString = "hello");
Both the strings are "hello";
Single line lambdas must be a value, so this:
get => Current = this[0];
Assigns the Current to be the first item in the list and then gives the first item in the list to the get, to be returned. It's like saying:
get {
Current = this[0];
return Current;
}
Your Current is now the first item in the list, just because you accessed the First property
I have a Use Case where I want to validate a specific property in a list of objects to make sure it is unique. The basic setting can be seen in the code below.
class Program
{
static void Main(string[] args)
{
Directory myDirectory = new Directory("Interaction Design");
myDirectory.Books.Add(new Book("978-0-262-64037-4", "The Design of Everyday Things")); //Should be added
myDirectory.Books.Add(new Book("978-0-262-13474-3", "Designing Interactions")); //Should be added
myDirectory.Books.Add(new Book("978-0-262-13474-3", "Whoops, I slipped up")); //Should NOT be added
}
}
public class Directory
{
public Directory(string name)
{
Name = name;
}
public string Name { get; set; }
public List<Book> Books { get; set; } = new List<Book>();
}
public class Book
{
public Book(string isbn, string title)
{
Isbn = isbn;
Title = title;
}
public string Title { get; set; }
public string Isbn { get; set; }
}
Now, in the code above, adding a new Book in the List of Books should throw an exception if the ISBN number isn't unique.
I would like to extend on the .Add() method of the List and add that validation, but I'm not sure how to actually do that.
I've seen similar things, but they all assume that the Directory inherits from List and you write an overriding .Add method to the Directory - which doesn't look like a valid solution in this case.
Perhaps my general approach is backwards?
Please advice.
If you want uniqueness, use a collection that gives you that: a set.
Make an IEqualityComparer<book> that considers two books to be equal if the Isbns match and use it in a HashSet<Book> that represents your unique list of books:
public class Directory
{
public HashSet<Book> Books { get; }
= new HashSet<Book>(new BookEqualityComparer());
//...
private class BookEqualityComparer : IEqualityComparer<Book>
{
public bool Equals(Book x, Book y)
{
if (ReferenceEquals(x, y))
return true;
if (ReferenceEquals(x, null) ||
ReferenceEquals(y, null))
return false;
return x.Isbn == y.Isbn;
}
public int GetHashCode(Book obj)
=> obj.Isbn.GetHashCode();
}
}
And you are done, you can't have any duplicate books in Books.
One option is to create a new class and inherit from Collection<T> instead of List<T>, where you could overwrite InsertItem
e.g.
public class MyList: Collection<string>
{
protected override void InsertItem(int index, string newItem)
{
DoValidation();
}
}
called with the following call:
var myList = new MyList();
myList.Add("item1");
Why not this?
public class Directory
{
private List<Book> _books;
public IEnumerable<Book> Books
{
{
return _books;
}
}
Public void AddBook(Book book)
{
//Validate
if(valid)
{
_books.Add(book);
}
}
//Etc
}
I have a need for customizing creation of a collection, with quite complicated relationships between the objects within it, and I can't figure out how to do it correctly.
For the sake of this issue, let's assume I'm working on a todo app. It has Items and SubItems, and the items have a week number indicating when they should be done:
public class Item {
public string Name { get; set; }
public int Week { get; set; }
public ICollection<SubItem> SubItems { get; set; }
}
public class SubItem {
public string Name { get; set; }
public Item Parent { get; set; }
}
Now, because this is what data usually looks like in the actual application, I want to create a collection of Items that has the following properties:
There are items that have the same name, but different weeks
There are items that have the same week but different name
There are sub-items that have the same name, but different parents
In order to do this, I've created a TodoItemSpecimenBuilder : ISpecimenBuilder which starts its Create method like this:
var type = (request as PropertyInfo)?.PropertyType ?? request as Type;
if (type == null || !typeof(IEnumerable<Item>).IsAssignableFrom(type))
{
return new NoSpecimen();
}
// build up the actual collection
return BuildActualCollection();
However, when I run tests with this specimen builder included in my context, I get lots (maybe 20 or 30) hits on the return statement before I enter even my setup code, and the first time I try to actually CreateMany<Item>(), it blows up with a cast exception because it can't cast OmitSpecimen to Item.
What am I doing wrong here?
Full sample code, compilable after installing NUnit and AutoFixture:
public class TodoList
{
public ICollection<Item> Tasks { get; set; }
}
public class Item
{
public string Name { get; set; }
public Week Week { get; set; }
public ICollection<SubItem> SubItems { get; set; }
public int ItemId { get; set; }
public TodoList TodoList { get; set; }
}
public class SubItem
{
public Item Item { get; set; }
public string Name { get; set; }
public int SortOrder { get; set; }
public string HelpText { get; set; }
}
public class Week
{
public int WeekId { get; set; }
}
public class ItemCollectionSpecimenBuilder : ISpecimenBuilder
{
public object Create(object request, ISpecimenContext context)
{
if (!IsApplicable(request))
{
return new NoSpecimen();
}
var items = new List<Item>(3);
var week1 = context.Create<Week>();
var week2 = context.Create<Week>();
items.Add(CreateItem(context, week1));
items.Add(CreateItem(context, week1));
items.Add(CreateItem(context, week2));
items.GroupBy(t => t.Week).ToList().ForEach(ConfigureNames);
ConfigureSubItems(context, items);
return items;
}
private static bool IsApplicable(object request)
{
bool IsManyItemsType(Type type) => typeof(IEnumerable<Item>).IsAssignableFrom(type);
bool IsItemsType(Type type) => type != null && typeof(Item) == type;
switch (request)
{
case PropertyInfo pInfo:
return IsManyItemsType(pInfo.PropertyType);
case Type type:
return IsManyItemsType(type);
case MultipleRequest multipleRequest:
if (!(multipleRequest.Request is SeededRequest seededRequest))
{
return false;
}
return IsItemsType(seededRequest.Request as Type);
default:
return false;
}
}
private static Item CreateItem(ISpecimenContext context, Week week)
{
var item = context.Create<Item>();
item.Week = week;
return item;
}
private static void ConfigureNames(IEnumerable<Item> items)
{
string name = null;
foreach (var item in items)
{
if (name == null)
{
name = item.Name;
}
else
{
item.Name = name;
}
}
}
private static void ConfigureSubItems(ISpecimenContext context, IEnumerable<Item> items)
{
foreach (var group in items.GroupBy(item => item.Week.WeekId))
{
var subItemTemplates = context.CreateMany<SubItem>().ToList();
foreach (var item in group)
{
item.SubItems.Clear();
foreach (var subItem in context.CreateMany<SubItem>().Zip(subItemTemplates,
(model, subItem) =>
{
subItem.Item = item;
subItem.Name = model.Name;
subItem.SortOrder = model.SortOrder;
subItem.HelpText = model.HelpText;
return subItem;
}))
{
item.SubItems.Add(subItem);
}
}
}
}
}
[TestFixture]
public class AutoFixtureSpecimenBuilderTests
{
private static void TestCreationOfTasks(Func<IFixture, ICollection<Item>> creator)
{
var fixture = new Fixture();
fixture.Customizations.Add(new ItemCollectionSpecimenBuilder());
fixture.Behaviors.OfType<ThrowingRecursionBehavior>().ToList()
.ForEach(b => fixture.Behaviors.Remove(b));
fixture.Behaviors.Add(new OmitOnRecursionBehavior());
var tasks = creator(fixture);
Assert.AreEqual(3, tasks.Count);
Assert.AreEqual(2, tasks.GroupBy(t => t.Week).Count());
Assert.IsTrue(tasks.GroupBy(t => t.Week).Select(g => g.Select(t => t.Name).Distinct()).All(distinctNames => distinctNames.Count() == 1));
var task = tasks.GroupBy(t => t.Week).OrderBy(g => g.Count()).First().OrderBy(t => t.ItemId).First();
}
[Test]
public void CreateMany() => TestCreationOfTasks(fixture => fixture.CreateMany<Item>().ToList());
[Test]
public void CreateWithProperty() => TestCreationOfTasks(fixture => fixture.Create<TodoList>().Tasks);
[Test]
public void CreateAsList() => TestCreationOfTasks(fixture => fixture.Create<IList<Item>>());
}
I can't think of any particularly good way to address this issue. The problem is that Item is a recursive (tree-like) data structure, and while AutoFixture does have some support for such, it's not easily extensible.
When you create an ISpecimenBuilder, you tell AutoFixture that this object is going to handle requests for particular objects. This means that you can no longer use the context to request those objects, because that'll recurse back into the same builder, causing an infinite recursion.
So, one option is to build up the objects 'by hand' from within the builder. You can still request all other types, but you'll have to avoid requesting objects that cause recursion.
Another option is to add a post-processor. Here's a proof of concept:
public class ItemCollectionSpecimenCommand : ISpecimenCommand
{
public void Execute(object specimen, ISpecimenContext context)
{
var #is = specimen as IEnumerable<Item>;
if (#is == null)
return;
var items = #is.ToList();
if (items.Count < 3)
return;
var week1 = context.Create<Week>();
var week2 = context.Create<Week>();
items[0].Week = week1;
items[1].Week = week1;
items[2].Week = week2;
items.GroupBy(t => t.Week).ToList().ForEach(ConfigureNames);
}
private static void ConfigureNames(IEnumerable<Item> items)
{
string name = null;
foreach (var item in items)
{
if (name == null)
name = item.Name;
else
item.Name = name;
}
}
}
You can configure your fixture like this:
var fixture = new Fixture();
fixture.Customizations.Add(
SpecimenBuilderNodeFactory.CreateTypedNode(
typeof(IEnumerable<Item>),
new Postprocessor(
new EnumerableRelay(),
new CompositeSpecimenCommand(
new AutoPropertiesCommand(),
new ItemCollectionSpecimenCommand()))));
fixture.Behaviors.OfType<ThrowingRecursionBehavior>().ToList()
.ForEach(b => fixture.Behaviors.Remove(b));
fixture.Behaviors.Add(new OmitOnRecursionBehavior());
This'll pass the repro tests CreateWithProperty and CreateAsList, but not CreateMany.
For various (historical) reasons, the way that CreateMany works is quite different from the way that something like Create<IList<>> works. If you really need this to work for CreateMany as well, I'll see what I can do, but I can't promise that this'll be possible at all.
After having looked at this repro for a few hours, this is the best I can come up with. I haven't really used AutoFixture for a year or two now, so it's possible that I'm simply out of shape, and that a better solution is available... I just can't think of it...
An object EmployeeInfoDocument contains a list of groups. Each group holds a list of employees. I am trying to use a nested foreach loop to write the values out to an XML file. It seems that I have the syntax wrong on the inner foreach loop. Visual Studio is giving me the following error:
foreach statement cannot operate on variables of type 'InfoObjects.Group' because 'InfoObjects.Group' does not contain a public definition for 'GetEnumerator'
How do I pull info from each employee within the groups? I'm extremly new to C#.
Any insight would be appreciated.
namespace InfoObjects
{
public class EmployeeInfoDocument
{
private List<Group> _groups = new List<Group>();
public List<Group> Groups
{
get { return _groups; }
set { _groups = value; }
}
}
public class Group
{
private string _text = string.Empty;
private List<Employee> _info = new List<Employee>
public Group()
{
}
public string Text
{
get {return _text; }
set { _text = value; }
}
public List<Employee> Employees
{
get { return _info }
set { _info = value; }
}
}
public class Employee
{
private string _name = string.Empty;
private string _department = string.Empty;
public Employee()
{
}
public string Name
{
get { return _name;}
set { _name = value;}
}
{
public string Department
{
get { return _department};
set { _department = value;}
}
}
}
namespace UI
{
public partial class Mainform: Form
{
private void OnSave(object sender, EventArgs e)
{
SaveFileDialog saveFileDialog = new SaveFileDialog();
saveFileDialog.RestoreDirectory = false;
saveFileDialog.InitialDirectory = Assembly.GetExecutingAssembly().Location;
saveFileDialog.Filter = "Employee Files|*.xml";
saveFileDialog.DefaultExt = "xml";
using (saveFileDialog)
{
if (saveFileDialog.ShowDialog(this) != DialogResult.OK)
return;
}
using (XmlWriter xmlw = XmlWriter.Create(saveFileDialog.FileName))
{
EmployeeInfoDocument k = new EmployeeInfoDocument();
List<Group> myList = k.Groups;
xmlw.WriteStartDocument();
xmlw.WriteStartElement("EmployeeInfo");
foreach (Group group in EmployeeInfoMgr.Document.Groups)
{
xmlw.WriteStartElement("Group Name");
xmlw.WriteString(group.Text);
foreach (Employee employee in group)
{
xmlw.WriteStartElement("Employee Name");
xmlw.WriteString(employee.Name);
xmlw.WriteEndElement();
xmlw.WriteStartElement("Employee Department");
xmlw.WriteString(employee.Department);
xmlw.WriteEndElement();
}
xmlw.WriteEndElement();
}
xmlw.WriteEndElement();
xmlw.WriteEndDocument();
}
}
}
}
Thanks again. I've been stuck on this for hours now.
Edited: Missing semi-colons (Terribly sorry!)
So this is your problem:
foreach (Group group in ...)
{
...
foreach (Employee employee in group)
{
}
}
The compiler can't use foreach over group because it has no idea what you'd iterate over. You either need to implement IEnumerable<T> (or at least provide an appropriate GetEnumerator() method) or specify that you actually want to iterate over the employees in the group:
foreach (Employee employee in group.Employees)
The latter is less of a change, of course, but if you want to be able to just iterate over a group, you can implement it like this:
public class Group : IEnumerable<Employee>
{
public string Text { get; set; }
public List<Employee> Employees { get; set; }
public IEnumerator<Employee> GetEnumerator()
{
return Employees.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
Notes:
I've replaced the properties with automatically-implemented properties, given that you were just using a field in a trivial way
I've removed the parameterless constructor which the compiler will provide by default
When implementing IEnumerable<T>, you also need to implement the non-generic IEnumerable, which I've done with explicit interface implementation as the GetEnumerator method clashes with the one in IEnumerable<T>.
At the moment there's very little encapsulation here - the list can be changed directly, for example. You could consider hiding the list, but keeping a field for the list (initializing it yourself) and having an Add(Employee) method or whatever you require.
In my C# .Net 4.0 composite pattern I want to have leafs that are generic. Most examples I found have a generic in the base node which propagates through the whole composite tree. I do not want that.
I have found the following solution (which I've stripped a bit to the essentials). An interface called INode which has two implementations. One called category which basically is a dictionary of INodes. It is a dictionary because I do not want duplicate leafs. The other implementation called ValueNode holds the information.
This allows for differently typed leaf nodes.
public interface INode
{
string Name { get; }
}
public class CategoryNode : INode
{
public CategoryNode(string name)
{
this.Name = name;
this.Children = new Dictionary<string, INode>();
}
public string Name { get; private set; }
public List<string> Keys
{
get { return this.Children.Keys.ToList(); }
}
private Dictionary<string, INode> Children { get; set; }
public INode this[string key]
{
get { return this.Children[key]; }
}
public void Add(INode node)
{
this.Children.Add(node.Name, node);
}
}
public class ValueNode<T> : INode
{
public ValueNode(
string name,
T defaultValue)
{
this.Name = name;
this.Value = this.Default = defaultValue;
}
public ValueNode(
string name,
T defaultValue)
{
this.Name = name;
this.Value = this.Default = defaultValue;
}
public T Default { get; private set; }
public T Value { get; set; }
public string Name { get; private set; }
}
Notice that I've made the children list private so nobody can remove nodes.
I am comfortable with this solution. However, the usage syntax it produces is a bit talkative. For example:
((this.root["category"] as CategoryNode)["leaf"] as ValueNode<int>).Value = (node as ValueNode<int>).Value;
While I had envisioned something like
this.root["category"]["leaf"] = node;
Does anybody have ideas for me to simplify the syntax?
How about adding an extension method to INode type ?
public static class INodeExtensions
{
public static void SetValue<T>(this INode node, string key, T v)
{
if(v is INode)
{
// category node set value
if(node is CategoryNode)
{
// convert and set value
}
else
{
throw new Exception("No children found.");
}
}
else
{
// value node set value
}
}
}
What about using a parameter array to specify the "path" to your leaf?
Optionally, there is another method in case you need to get a category node.
class CategoryNode : INode
{
public CategoryNode GetCategoryNode(params string[] path) {
CategoryNode cat = (CategoryNode)this.Children[path[0]];
for (int i = 1; i < path.Length; ++i) {
cat = (CategoryNode)cat.Children[path[i]];
}
return cat;
}
public ValueNode<T> GetLeafNode<T>(params string[] path) {
INode first = this.Children[path[0]];
if (path.Length == 1 && first is ValueNode<T>) return (ValueNode<T>)first;
CategoryNode cat = (CategoryNode)first;
for (int i = 1; i < path.Length - 1; ++i) {
cat = (CategoryNode)cat.Children[path[i]];
}
return (ValueNode<T>)cat.Children[path[path.Length-1]];
}
}
You use it like this:
var leafNode = root.GetLeafNode<int>("cat1", "cat2", "leaf");
// or
root.GetLeafNode<int>("cat1", "cat2", "leaf").Value = 1234;
The indexer is no longer needed.
I ended up with what Teddy proposed and also added a GetValue.
In addition, I put the indexer in the INode interface and just throw an exception when it is called on a value node. This way you can also use the this.root["category"]["leaf"] syntax.
You still must cast to a ValueNode<> if you want to access the value property though. But you can do this.root["category1"]["category2"].SetValue<int>("leaf", 42).