I've been struggling with the MVVM concept a lot. I am developing a remote file browser, so I can manage files on another computer from my own. I have a button that will open a new tab, so you can have multiple tab views of the files. When a new tab is opened, it should send a request to the client to get a list of drives, which will then be displayed in the newly created tab.
The way I am currently doing this is almost certainly not correct MVVM. I am using a Button_Click to enqueue the item count of the SubTabControl in a global queue in my code-behind. Then, I proceed to send the command to the client to request the list of drives.
private void Button_Click(object sender, RoutedEventArgs e)
{
_DrivesUpdateQueue.Enqueue(SubTabControl.Items.Count);
_ems.Clients[0].Send(_ems.ModuleName, (byte)Commands.GetDrives, String.Empty);
}
Once I receive the command, I add each drive to the ViewModel by casting the SubTabControl to my view model with the index.
public void UpdateDrives(ModularPacket packet)
{
_drives.Clear();
Debug.WriteLine("Drives! Drive size is " + Marshal.SizeOf(typeof(Drive)));
int index = _DrivesUpdateQueue.Dequeue();
for (int i = 0; i < packet.BodyLength; i += Marshal.SizeOf(typeof(Drive)))
{
Drive d = Utils.ByteArrayToStructure<Drive>(packet.Body, i);
string x = ParseSize(d.freeSpace);
((TabItemViewModel)SubTabControl.Items[index]).Items.Add(d);
}
}
Main XAML
<mah:MetroWindow x:Class="FileExplorerModuleServer.FileExplorerModuleServerUI"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mah="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro"
xmlns:local="clr-namespace:FileExplorerModuleServer"
mc:Ignorable="d" Title="File Explorer" Height="398.809" Width="653.198">
<Window.DataContext>
<!--Set the DataContent to be an instance of our view-model class -->
<local:MainWindowViewModel/>
</Window.DataContext>
<Grid>
<TabControl Name = "MasterTabControl" HorizontalAlignment="Stretch" Margin="10,10,0,0" VerticalAlignment="Stretch">
<TabItem Header="Browse Files">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!--The Command of the button is bound to the View Model -->
<Button Grid.Row="0" HorizontalAlignment="Right" Width="100"
Content="New Tab"
Command="{Binding NewTabCommand}" Click="Button_Click" />
<!--ItemsSource is bound to the 'Tabs' property on the view-
model, while DisplayMemeberPath tells TabControl
which property on each tab has the tab's name -->
<TabControl Name="SubTabControl" Grid.Row="1"
ItemsSource="{Binding Tabs}"
DisplayMemberPath="Header" SelectedIndex="{Binding SelectedIndex}">
<TabControl.ContentTemplate>
<DataTemplate>
<!--The UserControl/Grid were pointless, so I
removed them. ItemsSource of the ListView is
bound to an Items property on each object in
the Tabs collection-->
<ListView ItemsSource="{Binding Items}">
<ListView.View>
<GridView>
<GridViewColumn Header="Name" DisplayMemberBinding="{Binding Name}"/>
<GridViewColumn Header="Type" DisplayMemberBinding="{Binding Type}"/>
<GridViewColumn Header="Total Size" DisplayMemberBinding="{Binding TotalSize}"/>
<GridViewColumn Header="Free Space" DisplayMemberBinding="{Binding FreeSpace}"/>
</GridView>
</ListView.View>
</ListView>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</Grid>
</TabItem>
</TabControl>
</Grid>
</mah:MetroWindow>
View Models:
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
namespace FileExplorerModuleServer
{
public class MainWindowViewModel : ViewModelBase
{
public int SelectedIndex
{
get => _selectedIndex;
set => Set(ref _selectedIndex, value);
}
private int _selectedIndex;
// store our list of tabs in an ObservableCollection
// so that the UI is notified when tabs are added/removed
public ObservableCollection<TabItemViewModel> Tabs { get; }
= new ObservableCollection<TabItemViewModel>();
// this code gets executed when the button is clicked
public ICommand NewTabCommand
=> new RelayCommand(() =>
{
Tabs.Add(new TabItemViewModel() { Header = $"Loading..." });
SelectedIndex = Tabs.Count - 1;
});
}
public class TabItemViewModel : ViewModelBase
{
// this is the title of the tab, note that the Set() method
// invokes PropertyChanged so the view knows if the
// header changes
public string Header
{
get => _header;
set => Set(ref _header, value);
}
private string _header;
// these are the items that will be shown in the list view
public ObservableCollection<Drive> Items { get; set; }
= new();
}
}