C
C#11mo ago
pxd 丝

How could I implement the tree correctly using XAML?

Hello! I've started a XAML .NET application https://github.com/PerikiyoXD/RWTree/ The problem I'm facing is that I'm creating chunks of data in classes that are going to represent a tree. Now I just create TreeItem instances using this approach: Middleware/RenderWare/Stream/Chunks/ClumpStructChunk.cs
public new TreeViewItem ToTreeViewItem()
{
var treeViewItem = base.ToTreeViewItem();

treeViewItem.Items.Add(new TreeViewItem {Header = $"Atomic Chunk Count: {AtomicChunkCount}"});
treeViewItem.Items.Add(new TreeViewItem {Header = $"Light Chunk Count: {LightChunkCount}"});
treeViewItem.Items.Add(new TreeViewItem {Header = $"Camera Chunk Count: {CameraChunkCount}"});

return treeViewItem;
}
public new TreeViewItem ToTreeViewItem()
{
var treeViewItem = base.ToTreeViewItem();

treeViewItem.Items.Add(new TreeViewItem {Header = $"Atomic Chunk Count: {AtomicChunkCount}"});
treeViewItem.Items.Add(new TreeViewItem {Header = $"Light Chunk Count: {LightChunkCount}"});
treeViewItem.Items.Add(new TreeViewItem {Header = $"Camera Chunk Count: {CameraChunkCount}"});

return treeViewItem;
}
I know this is not the best approach and I'd like to have suggestions on how could I implement this The end product should be something like this (Side by side comparison, left is RWTree, right is RWAnalyze which I'm trying to replicate)
GitHub
GitHub - PerikiyoXD/RWTree: RWTree is a specialized application cra...
RWTree is a specialized application crafted for visualizing the internal structure of RenderWare binary streams. - GitHub - PerikiyoXD/RWTree: RWTree is a specialized application crafted for visual...
No description
38 Replies
pxd 丝
pxd 丝OP11mo ago
A must have feature is the icon on the tree, and to be fair, I'd like to know how to make it modular enought so I can extend it properly (If you need any reference please look at the repo, thank you!) (sorry for the lack of development in the statement but I have little time today, tomorrow I will answer the doubts and see the suggestions)
Denis
Denis11mo ago
XAML .net is not an application type, afaik. Do you mean WPF?
pxd 丝
pxd 丝OP11mo ago
Yep My bad So, I have a Base Class "Chunk" that will have derived classes of it "AtomicChunk", "ClumpChunk"... And the data is there. It's structured in a tree by itself (As ClumpChunk has ClumpStructChunk, FrameListChunk etc...) The thing is that I created an Interface that has a function that Chunks need to implement So far, I create the TreeItems manually But I think this isn't the correct way to do this
Sir Rufo
Sir Rufo11mo ago
Here is an example https://wpf-tutorial.com/treeview-control/treeview-data-binding-multiple-templates/ Your class design is also a bit odd. The models should not have anything to do with the UI. As a rule of thumb: If you use a UI type in your ViewModels and below then you have left the MVVM pattern (which is all about separation) Also you should avoid bidirectional parent-child relations. Let the child know about the parent or the paerent about the child. It will simplify your design.
br4kejet
br4kejet11mo ago
Does the data like ClumpChunk and ClumpStructChunk represent a hierarchy of items, as in, it could contain children of different or even the same type containing the children list? Not very farmiliar with render ware's stuff, cool that people are still using it though lol
pxd 丝
pxd 丝OP11mo ago
Yes, it's just like a tree, the chunks inside are defined by the chunk type RenderWare game modder here 🚀
br4kejet
br4kejet11mo ago
And you want to replecate that entire tree in the view?
pxd 丝
pxd 丝OP11mo ago
Exactly The idea is, I have a parser that will read the binary and will just generate a representation using objects, in this case, Chunk derived classes Then, in paralell, I want to represent that structure using the data I've parsed I thought I could just hack together a "TreeItem generation routine"
br4kejet
br4kejet11mo ago
I assume the tree can be modified by the user right? As in adding/removing nodes from the tree
pxd 丝
pxd 丝OP11mo ago
Yes, it would be modified by removing or adding items, basically crafting one youself from scratch But I don't need that functionality right away If that's already added on the first step would be awesome So far I need to understand how DataBinding works and how could I represent a "TreeItem" with a "Chunk" Right now is a hack as I said
br4kejet
br4kejet11mo ago
This goes heavily against MVVM but it's the easiest solution tbh, what you could do is create NodeAdded and NodeRemoved events in the classes that have that hierarchial feature, and then you could create custom type for those models (extending TreeViewItem) And then those custom tree view item classes would contain a method like AttachModel and DetatchModel, and in attach you would hook event handlers to the model and also generate all child tree view items based on the model's children (and you'd need some sort of factory method to create the custom tree view item class based on the model; i would just use a Dictionary<Type, Func<ChunkTreeViewItem>> for simplicity) You would have a control called something like RootTreeView, which contains a reference to the root chunk model, and when that property changes, it generates its children in the same way as the tree view items, and calls the AttachModel/DetatchModel for them. The AttachModel would also create the child tree items too so it's recursive
pxd 丝
pxd 丝OP11mo ago
So right now, I have a Model and a View, The custom TreeItem would be the ViewModel
br4kejet
br4kejet11mo ago
If that's what you want then yeah
pxd 丝
pxd 丝OP11mo ago
I'm trying to locate myself between your advice and what I already have Is this assertion right?
br4kejet
br4kejet11mo ago
I decided to avoid view models because it just adds extra complexity, like having to replicate all model types as view models
pxd 丝
pxd 丝OP11mo ago
I don't mind having a bit of boilerplate tbh, MVVM isn't my favourite anyways
br4kejet
br4kejet11mo ago
But using view models, you wouldn't have to nessesarily create the custom UI controls as you coud just stick with the general TreeViewItem and use binding I have an example if the code would be easier to understand. The part of the app is called the ResourceManager, which manages a hierarchy of Resource nodes, which can be folders or items, and there are custom item types that extend the base item type https://github.com/AngryCarrot789/FramePFXNewFrameworkTests/tree/master/FramePFX/Editors/Controls/Resources Those are the UI parts in that folder And then the models are in this folder https://github.com/AngryCarrot789/FramePFXNewFrameworkTests/tree/master/FramePFX/Editors/ResourceManaging The folder model contains the Added/Removed/Moved (for efficiency over remove then add) events, and the view listens to them and creates/removes/moves the UI components accordingly Oh i haven't actually pushed the tree UI parts to it yet lmfao Maybe the model side would help though. You have to do all the binding yourself, either by making models implement INotifyPropertyChanged, or by handling individual events which is more tedious but in some cases can be so much better And then the icons you were talking about, in your control templates for the custom tree items, you could just have an Image called soemthing like PART_IconImage, and in OnApplyTemplate you would fetch it and use that to update the icon based on however you determine the icon
pxd 丝
pxd 丝OP11mo ago
I'm having a look to that code
br4kejet
br4kejet11mo ago
I can't really remember I did the whole attach/detach thing instead of just making it a dependency property, it might have just been for performance but I can't really remember In other parts of the app I also have OnAdding, OnAdded, OnRemoving, OnRemoved instead which get called before and after actually adding/removing the controls into/from the items collection, but I only do that just as a guarentee that the model object isn't null when the object is actually in the list, just in case inserting the UI item causes some method to get called which didn't expect the model to be null
pxd 丝
pxd 丝OP11mo ago
I understand the Event-oriented part I guess most of the heavy lifting is already there using WPF events?
br4kejet
br4kejet11mo ago
Heavy lifting?
pxd 丝
pxd 丝OP11mo ago
Most of the events* Well, I need to learn WPF in depth first, because it seems that I need to create "Components" and then bind them with the Models My approach I see it's "Data binding via Code-behind" I see now that my Models would need implemeting INotifyPropertyChanged as you said
br4kejet
br4kejet11mo ago
Binding or attaching, is just associating the control with that particular model. In my app I update the model object for the root object (ResourceExplorerListControl), and in its property changed callback, I then remove event handlers for the old model and clear the items, and add handlers for the new model and add all of the model's items. Because in my case I just have a single control type (ResourceListItemControl), that add phase just consists of creating a new instance of that control, calling OnAdding, then inserting that control into the current control's item list, and then calling OnAdded, if that makes sense If you wanted to, you could just use a single custom class that extends TreeViewItem, but then update its Header property with a custom control based on your model type, which is probably better that loads of custom TreeViewItem types But yeah INotifyPropertyChanged would make things easier, but at that point you're basically treating the model as a view model too which I guess is fine If you decide to do that approach with a model and view model in one, then instead of a custom control for the tree item content, you could just use data templates and bind to properties in your model (making sure to set the DataContext of the tree view items accordingly obviously)
pxd 丝
pxd 丝OP11mo ago
I agree, I could just have a custom type that will have an Icon, and a Title, no more Then styles to handle selection and maybe some other cases https://github.com/PerikiyoXD/RWTree/blob/459c62d298b5adeeef09b34664826beff3041c15/MainWindow.xaml.cs#L14 That's the Data per-se Then I do this: https://github.com/PerikiyoXD/RWTree/blob/459c62d298b5adeeef09b34664826beff3041c15/MainWindow.xaml.cs#L84 Then DFF does call the hierarchy to fetch the nodes from the children:
public TreeViewItem ToTreeViewItem()
{
TreeViewItem tree = new() { Header = "DFF" };

TreeViewItem clumpTree = Clump.ToTreeViewItem();
tree.Items.Add(clumpTree);

return tree;
}
public TreeViewItem ToTreeViewItem()
{
TreeViewItem tree = new() { Header = "DFF" };

TreeViewItem clumpTree = Clump.ToTreeViewItem();
tree.Items.Add(clumpTree);

return tree;
}
That's the dynamic aspect here How would I start? Implementing my own TreeViewItem with a "ChunkTreeViewItem" type?
br4kejet
br4kejet11mo ago
That's what I'd do In your main window's LoadFile is where I would replace the code with this:
Dff = dffReader.Dff;
var tree = CreateTreeForDFF(Dff);
this.NodeTree.ItemsSource = tree.Items
Dff = dffReader.Dff;
var tree = CreateTreeForDFF(Dff);
this.NodeTree.ItemsSource = tree.Items
pxd 丝
pxd 丝OP11mo ago
Do I need to derive from a base class and attach Interfaces?
br4kejet
br4kejet11mo ago
public static TreeViewItem CreateTreeForDFF(Dff model)
{
TreeViewItem root = new TreeViewItem() { Header = "DFF" };
foreach (var item in model.Children) {
CustomTreeViewItem treeItem = new CustomTreeViewItem();
treeItem.OnAddingToParent(root, item);
root.Items.Add(treeItem);
// if you do end up using OnApplyTemplate in CustomTreeViewItem, you might
// need to call these 2 methods first if OnAdded uses a template's controls
// treeItem.InvalidateMeasure();
// treeItem.ApplyTemplate();
treeItem.OnAdded(root, item);
}

return root;
}
public static TreeViewItem CreateTreeForDFF(Dff model)
{
TreeViewItem root = new TreeViewItem() { Header = "DFF" };
foreach (var item in model.Children) {
CustomTreeViewItem treeItem = new CustomTreeViewItem();
treeItem.OnAddingToParent(root, item);
root.Items.Add(treeItem);
// if you do end up using OnApplyTemplate in CustomTreeViewItem, you might
// need to call these 2 methods first if OnAdded uses a template's controls
// treeItem.InvalidateMeasure();
// treeItem.ApplyTemplate();
treeItem.OnAdded(root, item);
}

return root;
}
And then in CustomTreeViewItem I would do something like this:
public void OnAddingToParent(TreeViewItem parentItem, ModelObjectNotSure item)
{
if (item is ChunkThingModel chunk) {
foreach (var childItem in chunk.Children) {
CustomTreeViewItem treeItem = new CustomTreeViewItem();
treeItem.OnAddingToParent(this, childItem);
this.Items.Add(treeItem);
treeItem.OnAdded(this, childItem);
}
}
}
public void OnAddingToParent(TreeViewItem parentItem, ModelObjectNotSure item)
{
if (item is ChunkThingModel chunk) {
foreach (var childItem in chunk.Children) {
CustomTreeViewItem treeItem = new CustomTreeViewItem();
treeItem.OnAddingToParent(this, childItem);
this.Items.Add(treeItem);
treeItem.OnAdded(this, childItem);
}
}
}
Something along those lines if that makes sense That's what I do and it seems to work nicely
pxd 丝
pxd 丝OP11mo ago
I see
br4kejet
br4kejet11mo ago
If the model doesn't have children then you don't add anything to this.Items
pxd 丝
pxd 丝OP11mo ago
Now I gotta find out how to do the XAML part
br4kejet
br4kejet11mo ago
But then actually updating things like the headers, icons... that's where you could either do this.DataContext = item in OnAddingToParent (or OnAddedToParent, at this point it depends on whatever one works lol) and then bind in the XAML code assuming your models implement INotifyPropertyChanged Or, you add event handlers in stuff like OnAddedToParent and remove the handlers in OnRemovingFromParent to then update the header when a model's header changed event is fired Binding is probably simpler for your case though But your models will start looking pretty much like view models, unless you're fine with that
pxd 丝
pxd 丝OP11mo ago
In the future I want to extract the "Chunk" stuff to a library so it can be reused later I'd probably not bind the data to the view directly
br4kejet
br4kejet11mo ago
The event route might be the better options in that case
pxd 丝
pxd 丝OP11mo ago
Right now the data classes are just implementing IBinaryReadWrite:
public interface IBinaryReadWrite
{
public void Read(BinaryReader binaryReader);
public void Write(BinaryWriter binaryWriter);
}
public interface IBinaryReadWrite
{
public void Read(BinaryReader binaryReader);
public void Write(BinaryWriter binaryWriter);
}
Then each chunk derived type implements that and the properties:
public class ChunkHeader
{
public uint Size = 0;
public ChunkType Type = ChunkType.Unknown;
public uint Version = 0;

private void Read(BinaryReader fileAccess)
{
Type = (ChunkType)fileAccess.ReadUInt32();
Size = fileAccess.ReadUInt32();
Version = fileAccess.ReadUInt32();
}

private void Write(BinaryWriter fileAccess)
{
fileAccess.Write((uint)Type);
fileAccess.Write(Size);
fileAccess.Write(Version);
}
...
}
public class ChunkHeader
{
public uint Size = 0;
public ChunkType Type = ChunkType.Unknown;
public uint Version = 0;

private void Read(BinaryReader fileAccess)
{
Type = (ChunkType)fileAccess.ReadUInt32();
Size = fileAccess.ReadUInt32();
Version = fileAccess.ReadUInt32();
}

private void Write(BinaryWriter fileAccess)
{
fileAccess.Write((uint)Type);
fileAccess.Write(Size);
fileAccess.Write(Version);
}
...
}
In that sense the models are straightforward And I'd not mix the ViewModel here for cleanness sake
br4kejet
br4kejet11mo ago
I do similar stuff in my code and decided that Reading is part of the object construction phase and therefore has no reason to fire events for data changing
pxd 丝
pxd 丝OP11mo ago
I'd prefer not binding the Read to the Ctor as I'd need to have empty defaults That's by design
br4kejet
br4kejet11mo ago
I just meant that, say a model was attached to a tree item in the view, calling Read on that model might just corrupt the UI a bit But could allow that though, Read could fire the according events
pxd 丝
pxd 丝OP11mo ago
Read only happens when you load the file Save only happens when you save the file In this case, the tree will be reloaded As the state just changed Same when I manipulate any property I guess With events you can avoid it and I think that's the point you're telling
Want results from more Discord servers?
Add your server