C
C#8mo ago
stigzler

✅ WPF - how to approach a treeview displaying different types at different levels via MVVM

I'm just learning WPF - so seeking some design advice. I'm designing a Visual Studio extension to view and edit Gists. Each type Gist has a collection of type GistFiles (Files property in a Gist): I'll have 3 views inside the main View: GistsBrowser (user control=treeview), GistEditor (user control) and GistFileEditor (user control). I'm thinking I need to store the Collection of Gists in the MainViewModel, as different UserControls/ViewModels will need access to a shared GistsCollection to ensure synchronicity. For the GistsBrowser, I need to make a specific treeview control to display all Gists at the root level which can be expanded to show all of the GistFiles within a Gist. There will be no further child levels. Thus, this UserControl will need its own ViewModel (GistsBrowserViewModel). However, I'm stuck on a few things: How to structure the Gists collection in MainViewModel. Not sure if I need to map the Github data objects onto my own models, or map them directly onto a viewmodel. Do I need a Gist/GistFile viewmodel? Also, this links with: What data construct to use for the Gists collection and how to hook it up to a TreeView (given level 0 will be Gists and level 1 GistFiles) I hope that makes some kind of sense. Any pointers for further reading/research would be appreciated.
52 Replies
Denis
Denis8mo ago
To display data in a tree view you need to use hierarchical data templates To display different data with their respective views define multiple hierarchical data templates in the resources tag of your treeview You do not need a gistviewmodel per se
stigzler
stigzlerOP8mo ago
Thanks Denis. Funnily enough you type that just as I'm watching this: https://youtu.be/U2ZvZwDZmJU?si=13ERFuQ2zmHBFlOB&t=3617
AngelSix
YouTube
C# WPF UI Tutorials: 03 - View Model MVVM Basics
Part of a series of tutorials on creating WPF applications in C# Converts the previous TreeView demo application from code-behind to much better View Model MVVM application Source code here: https://github.com/angelsix/youtube
stigzler
stigzlerOP8mo ago
So I'm thinking, my TreeViewModel would have a list of Gists, each of which contains a List of GistFiles. The DataBinding through the Hierarchical data template would bind to List<GistFile> and that should display items correctly? forgive me - WPF/MVVM fries my brain a little but I would need a GistViewModel and a GistFileView model as well as I understand it?
Denis
Denis8mo ago
Will the data be retrieved in real time and be updated constantly? Do you want to update the data or only read it?
stigzler
stigzlerOP8mo ago
Will retrieve the list of gists from github. Then, if the user changes data on the gist or gistfile detail views, then an indicator will show on a Save icon. The user will manually select save That's the easy part!
Denis
Denis8mo ago
It's ok, I've watched these tutorials too some time ago, and they fried my brain too
stigzler
stigzlerOP8mo ago
🤣 I think I;'m conceptusally there, tbh. Just need to give it a go One thing I'm puzzled about is where I keep the ObservableCollection<GistViewModel> - I think I need to keep it in the MainViewModel, as teh GIstsBrowser, GistEditor and GistFileEditor will all need to access it i.e all need to access the same data models or dataviewmodels more speciically does that sound about right?
Denis
Denis8mo ago
Here are a few points. Your viewmodel is the main source of data that comes in contact with a view. Viewmodels expose models into the view, that represent data. There is no rule that would state - if a model must be observable, then you need a viewmodel to wrap it. So, here's a simple rule of thumb: what has logic and binds to the view is a viewmodel What represents data and can, but doesn't necessarily have to observable is a model I need to help with cooking, will come back to you l8r
stigzler
stigzlerOP8mo ago
thanks
Denis
Denis8mo ago
Back. Alright, will your application be a single window? Will you have multiple pages, dialog windows, etc.? If it'll be a single window, then you can significantly simplify everything You can have a MainWindow.xaml (or I usually rename it to MainWindowView, but be careful with that, VS can break your code after the rename) and a single MainWindowViewModel
stigzler
stigzlerOP8mo ago
yep - essentially single window (it's a VS ext - so will all be in a single tool Window)
stigzler
stigzlerOP8mo ago
source here if it helps: https://github.com/stigzler/VisGist
GitHub
GitHub - stigzler/VisGist
Contribute to stigzler/VisGist development by creating an account on GitHub.
Denis
Denis8mo ago
the viewer, editor and file editor will all be within one window, right?
stigzler
stigzlerOP8mo ago
yep
Denis
Denis8mo ago
and will be visible to the user at the same time? or will be switched between view -> editor -> file editor
stigzler
stigzlerOP8mo ago
at the moment - yes - GistsBrowser, GistEditor and GistFileEditor will all be visible at the same time
Denis
Denis8mo ago
Let me dig a little deeper. Given I open your tool, sign-in to GitHubs gist provider. I will presumably then see a list of my Gists via the GistsBrowser. By selecting a gist within the browser, its contents will be revealed in the GistEditor, correct? Plus, another question, out of curiosity, are you using Fody Weaver or Community Toolkit MVVM?
stigzler
stigzlerOP8mo ago
Kind of. The tree view's root elements will be essentially be the Github Gist data object. You'll expand that and it will list all of the GistFiles linked with that Gist (GistFiles belong to one Gist only). When you click a Gist, the editable detilas will opne in the gist editor (it's only two properties really) When you click a GistFile - likelwise will happen in the GistFile editor (again - only couple/few fields) just started using Community Toolkit MVVM not very fmailiar with it yet
Denis
Denis8mo ago
Very good, it actually helped me understand how to use MVVM properly
stigzler
stigzlerOP8mo ago
cool
Denis
Denis8mo ago
If it is to be within a single window, then use a single viewmodel you'll save yourself a ton of trouble
stigzler
stigzlerOP8mo ago
I'm thinking ObservableCollection<GistViewModel> in MainViewModel. GistVieModel contains ObservableCollection<GistFileViewModel>
Denis
Denis8mo ago
The cool thing about user controls is that when you place them within a window, or another user control, they inherit their parent's data context Make it Gist instead of GistViewModel because it is a model it shouldn't have any logic same goes for the GistFile my recommendation from what I've gathered so far So, you'll have a MainWindow.xaml + MainWindowViewModel as its data context within the MainWindow.xaml you'll create some sort of grid layout and place your browser, editor and file editor user controls
stigzler
stigzlerOP8mo ago
oh. Ok - so have a local Gist and GistFile Model? I'm goign to store the 'original' (i.e. imported) Gist in the model or vm (whichever I use) for reference
Denis
Denis8mo ago
Do you need to store the original? Is it for undo/redo? or "restore"?
stigzler
stigzlerOP8mo ago
fair point - I'll give that some thought - the update fucnton through the gItClient only need Ids etc - guess I won't need them
Denis
Denis8mo ago
You can have that as a property of the respective model You'll probably need it anyway - MainWindow.xaml - MainWindowViewModel.cs - Gist.cs - GistFile.cs - GistsBrowserView.xaml - GistsEditorView.xaml - GistsFileEditorView.xaml here's what I've gathered so far Do you want your collections to be "reactive", or in other words, do you want changes to collections within your models visible instantly during the runtime of your tool? If so, they must be observablecollections
stigzler
stigzlerOP8mo ago
only question is - when does a model become a view model? If I needed to include some logic in the data object (for example to generate a specific property) is it then a vm? For example, the Gist object is going to need some programmatic producting of its "Name" (or header in the TreeView) as it gets it from GistFile[0].Filename There is no 'name' in the Github Gist
Denis
Denis8mo ago
Models should be dumb objects containing data, that can be observable - so if you need logic, it must be a viewmodel. But again, should you really migrate your models into viewmodels? Where else can you place this logic?
stigzler
stigzlerOP8mo ago
yeah - I'm liking your tac - kiss principle
Denis
Denis8mo ago
Maybe a converter in the view? Or some logic in the MainWindowViewModel upon retrieving the data from the API?
stigzler
stigzlerOP8mo ago
yes - guess I could examine the gist in a converter as it would have the GistFile[0] within it
Denis
Denis8mo ago
Yeah, and separation of concerns
stigzler
stigzlerOP8mo ago
ok - this feels right. So keep try and keep to MainViewModel only where possible - Have an ObservabelCollection<Gist> in that vm and databind that to any browsers/editors
Denis
Denis8mo ago
Models represent data, viewmodels do logic - get data, and expose it to the view Yes
stigzler
stigzlerOP8mo ago
yeah - thanks - that's been really helpful. Dunno why I'm still struggling so much with WPF + MVVM - it's taking a mental shift that I'm struggling with
Denis
Denis8mo ago
public sealed partial class MainViewModel
: ObservableObject
{
/// <summary>
/// Determines whether the loading screen for loading gists is displayed
/// </summary>
[ObservableProperty] private bool _gistsLoading;

/// <summary>
/// Currently selected gist
/// </summary>
/// <remarks>
/// <c>null</c> when nothing is selected
/// </remarks>
[ObservableProperty] private Gist? _selectedGist;

public ObservableCollection<Gist> Gists { get; } = new();

// Bind this to the view Loaded event via interaction behaviors
[RelayCommand]
private async Task LoadGists()
{
LoadingGists = true;
try
{
// Load the fun stuff from Gist API
}
finally
{
LoadingGists = false;
}
}
}
public sealed partial class MainViewModel
: ObservableObject
{
/// <summary>
/// Determines whether the loading screen for loading gists is displayed
/// </summary>
[ObservableProperty] private bool _gistsLoading;

/// <summary>
/// Currently selected gist
/// </summary>
/// <remarks>
/// <c>null</c> when nothing is selected
/// </remarks>
[ObservableProperty] private Gist? _selectedGist;

public ObservableCollection<Gist> Gists { get; } = new();

// Bind this to the view Loaded event via interaction behaviors
[RelayCommand]
private async Task LoadGists()
{
LoadingGists = true;
try
{
// Load the fun stuff from Gist API
}
finally
{
LoadingGists = false;
}
}
}
Once everything clicks, you'll love it
stigzler
stigzlerOP8mo ago
That's awesome man and that code matches what's in my head! Thanks again - top bloke!
Denis
Denis8mo ago
And you put the rest in the viewmodels for loading the editor info and so on, and saving, etc. To organize stuff I usually use #regions. It's a controversial feature of C#, that some devs use to hide large potions on code within their methods.... But if used right, you can organize your code quite well Here's how I do it
public sealed partial class MainViewModel
: ObservableObject
{
#region Fields

/// <summary>
/// Determines whether the loading screen for loading gists is displayed
/// </summary>
[ObservableProperty] private bool _gistsLoading;

/// <summary>
/// Currently selected gist
/// </summary>
/// <remarks>
/// <c>null</c> when nothing is selected
/// </remarks>
[ObservableProperty] private Gist? _selectedGist;

#endregion

#region Properties

public ObservableCollection<Gist> Gists { get; } = new();

#endregion

#region Commands

// Bind this to the view Loaded event via interaction behaviors
[RelayCommand]
private async Task LoadGists()
{
LoadingGists = true;
try
{
// Load the fun stuff from Gist API
}
finally
{
LoadingGists = false;
}
}

#endregion

public MainWindowViewModel()
{
// I put the constructor here, I do not region it, untless there is a need for multiple ctors
}

#region Methods

// For non-command logic

#endregion
}
public sealed partial class MainViewModel
: ObservableObject
{
#region Fields

/// <summary>
/// Determines whether the loading screen for loading gists is displayed
/// </summary>
[ObservableProperty] private bool _gistsLoading;

/// <summary>
/// Currently selected gist
/// </summary>
/// <remarks>
/// <c>null</c> when nothing is selected
/// </remarks>
[ObservableProperty] private Gist? _selectedGist;

#endregion

#region Properties

public ObservableCollection<Gist> Gists { get; } = new();

#endregion

#region Commands

// Bind this to the view Loaded event via interaction behaviors
[RelayCommand]
private async Task LoadGists()
{
LoadingGists = true;
try
{
// Load the fun stuff from Gist API
}
finally
{
LoadingGists = false;
}
}

#endregion

public MainWindowViewModel()
{
// I put the constructor here, I do not region it, untless there is a need for multiple ctors
}

#region Methods

// For non-command logic

#endregion
}
Then you can collapse those regions to see what you need I use it to organize a C# class structure - not bodies of methods or properties!!!
stigzler
stigzlerOP8mo ago
ah yes - got region and comments on snippet shortcuts!
Denis
Denis8mo ago
and I use consistent names Ctrl+K, S I believe Not sure right now
stigzler
stigzlerOP8mo ago
Thanks again - you've put me on track - sure it'll be plain sailing form here (!!?? - what could possibly go wrong?)
Denis
Denis8mo ago
Navigation, communication between viewmodels, Dependency injection Clean viewmodel design - zero references to GUI framework elements Working with grouped, filtered, sorted collections in real time - collection view source And 🥁 localization So yeah.... there's a lot to learn lol
stigzler
stigzlerOP8mo ago
Plenty to keep me out of mischief
Denis
Denis8mo ago
Keep at it, you'll master it all in no time
stigzler
stigzlerOP8mo ago
tbh - this is my first "right I'm gonna learn WPF/MVVM if it kills me" project
Denis
Denis8mo ago
My first wpf project was total crap and my attempt at mvvm was horrendeous so, you're on the right track and a great project btw
stigzler
stigzlerOP8mo ago
and things are already starting to click (commands, no code in the xmal code-behind, get the basic databinding concepts) and I can sense that it's good once you get there
Denis
Denis8mo ago
the code behind still has a use in some cases but you'll figure them out soon
stigzler
stigzlerOP8mo ago
thanks - nicked the idea - I did try forking some virtual abandonware - but his code was bonkers - he had view models for everything - just couldn't abide it in the end, so decided to make this my learn WPF MVVM starter project - ocotkit is suprisingly easy to use - even for a hobby coder like me! Right - just gotta code it now - once more into the breach - thanks fella - amazing help
Denis
Denis8mo ago
Welcome, feel free to dm if you need more help Otherwise, $close
MODiX
MODiX8mo ago
Use the /close command to mark a forum thread as answered
Want results from more Discord servers?
Add your server