Prequisite
Audience
This article is for all the programmers who want to start working with simple applications in WPF/MVVM.
Even the beginners that have no experience with Visual Studio will find a step-by-step illustration of all the required operations.
Introduction
To manage the content of the graphical region in WPF, there are some tools (like PRISM) that allow you to have a main region which doesn't change during the navigation, and a dynamic one that will hold the content of the controls.
In some simple cases, we want to perform it in a simple way without using any tool. Fortunately, the Wpf Material design Toolkit http://materialdesigninxaml.net/ is providing great items that can be used as navigation menus.
In this article, I'm going to show you how to create an application and navigate between views using the tab control of the Material design toolkit in WPF.
Terminology
WPF
Windows Presentation Foundation is a graphical specification that started from dotNet 3.0. It's generally considered as the successor of Winforms and it's mainly used in the development of rich client application.
XAML
eXtensible Application Markup Language, is a language based on XML and used to define the views of the WPF applications
MVVM
Model View View Model, it's a pattern used in WPF application to ensure the seperation of concerns. Which means in an application we have:
- Models: That contains the definition of the entities.
- Views: The views contains the user interfaces, the fields, the forms .... and it's in xaml.
- ViewModels: Which contains all the logic and the processing related to a specific view.
Binding
The binding is the mechanism to attach a view or an element of a view to a data context, Mainly a view Model.
In other words, the view model will contains properties, the view will contains items: The binding is the magician that says the value of this item is linked to this property of the view model
Material Design
These are standards and rules defined by google to determine the aspect of the graphical elements in a user interface.
In our sample, we will use the WPF implementation of this standard: http://materialdesigninxaml.net/
TabControl
Tab Control is a graphical element that allows you to show several tabs that contain different contents
DataGrid
The data grid is a graphical element that allows you to show data in the form of a table with header. It's a kind of representation of the elements in the database.
StackPanel
A Stackpanel is a graphical element that allows to put other graphical element next to each other horizontally or vertically.
Solution Creation
The first thing you need to do is the creation of the project and the solution. We will simulate a small application of soccer to have some fun.
Open your visual studio then choose create a new project
Then click on WPF Application
Then create your solution. Generally I choose different names between the solution and the main application project: a global one for the solution, then a more specific one for the project.
Click Next (choose the netcore 6.0) then Create
Now you should have your solution created with a main WPF project, we want to seperate the concerns so we will create another project that will give us access to data.
For the moment we will just use a list. We will see how we will enrich it with a database in another article.
Creation of class library project
To create a new project in your solution right click on the solution then add a new project
Now you need to give your library a name
Now your solution explorer should look like this
Creation of Models and Data Access Method:
Now let's move to the more serious things. We need to add the Models, which are the entities that will contain the definition of our objects.
In our case, it will be Team and Player.
So add a solution folder and call it Models :
Then under Models you need to create two classes that will contain the definition of our entities.
In the same way that you created the folder Models, right click on the folder itself then choose add then "New Item" call your class Team and do it also for the class Player
The class Team is as follows:
public class Team
{
public string Name { get; set; }
public string Description { get; set; }
public string Town { get; set; }
public string Country { get; set; }
public override string ToString()
{
return Name;
}
}
The class Player is as follows:
public class Player
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string BirthDate { get; set; }
public int GoalsNumber { get; set; }
public Team Team { get; set; }
}
Then create a class called DataProvider, this one will be responsible on the management of the data and will expose the methods that will be called in the application
public class DataProvider
{
private DataProvider()
{
_players = new List<Player>();
}
private List<Player> _players { get; set; }
private static DataProvider _instance;
public static DataProvider Instance
{
get
{
if(_instance == null )
_instance= new DataProvider();
return _instance;
}
}
public List<Team> GetTeams()
{
return new List<Team>()
{
new Team(){Name="TS",Description="Tinja Sport",Town="Tinja",Country="Tunisia"},
new Team(){Name="SAMB",Description="Stade Africain Manzel bou Rguiba",Town="Manzel BouRguiba",Country="Tunisia"},
new Team(){Name="OB",Description="Olympique de Béja",Town="Béja",Country="Tunisia"},
new Team(){Name="CA",Description="Club Africain",Town="Beb Jedid",Country="Tunisia"},
};
}
public void AddPlayer(Player p)
{
_players.Add(p);
}
public List<Player> GetPlayers()
{
return _players;
}
}
Now you have an independent library which is reponsible for providing data, you need to reference it in the main application to be able to use it.
To do this :
- Go to the project SoccerManagement.Main and then you will find dependencies :
- Right click on dependencies then choose Add project reference
- Check the project SoccerManagement.DataProvider
The next step is to get the Material design library to be able to use a styled items, mainly the tab control.
Nuget Packages
Now you've created your data access layer (let's say a simulation of a real data access layer), we will work on the main project and the presentation layer.
So we need to reference the Material design libraries from Nuget So :
- Right click on the SoccerManagement.Main project
- Choose Manage Nuget Packages
- Then in the Browse Tab search for Material Design and then install them.
- After the installation you should have something like this
Creation of views and viewModel and binding
Now we will go back to the main project. The MainWindow.xaml will be the main unchanged view during the navigation and will contain a call to the other views.
So we will work on it after defining the views.
The first view is the one that adds a player to the list. In the xaml we define the textblocks, the labels ... and in the ViewModel we will define the needed logic and the properties.
The link between them is done using the DataContext (the view model is the dataContext of the view in our case) and the binding.
So to add a player we will create a UserControl (in the same way we created previous items : Add Item then choose UserControl) :
As you may note, in the TextBox.Text, there is an attribute called Binding. It's responsible on attaching the introduced value of the text to the Property in the view Model
For example we have a binding Path="Player.FirstName" it means that this value will be written in the attribute FirstName of the property Player in the ViewModel.
We told this user control that your viewModel Is AddPlayersViewModel in the .xaml.cs of the class
public partial class AddPlayerView : UserControl
{
public AddPlayerView()
{
InitializeComponent();
DataContext = new AddPlayersViewModel();
}
}
<UserControl x:Class="SoccerManagement.Main.Views.AddPlayerView"
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:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="600">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Horizontal" HorizontalAlignment="Stretch" Margin="10 20 0 0">
<materialDesign:PackIcon
Kind="Account"
Foreground="{Binding ElementName=NameTextBox, Path=BorderBrush}"/>
<TextBox Width="400"
x:Name="NameTextBox"
materialDesign:HintAssist.Hint="First name"
Style="{StaticResource MaterialDesignFloatingHintTextBox}"
>
<TextBox.Text>
<Binding Path="Player.FirstName" UpdateSourceTrigger="PropertyChanged" Mode="TwoWay"/>
</TextBox.Text>
</TextBox>
</StackPanel>
<StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Stretch" Margin="10 20 0 0">
<materialDesign:PackIcon
Kind="Account"
Foreground="{Binding ElementName=LastNameTextBox, Path=BorderBrush}"/>
<TextBox Width="400"
x:Name="LastNameTextBox"
materialDesign:HintAssist.Hint="Last name"
Style="{StaticResource MaterialDesignFloatingHintTextBox}"
>
<TextBox.Text>
<Binding Path="Player.LastName" UpdateSourceTrigger="PropertyChanged" Mode="TwoWay"/>
</TextBox.Text>
</TextBox>
</StackPanel>
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Stretch" Margin="10 20 0 0">
<materialDesign:PackIcon
Kind="BirthdayCake"
Foreground="{Binding ElementName=BirthDatePicker, Path=BorderBrush}"/>
<DatePicker x:Name="BirthDatePicker" Grid.Row="2" Style="{StaticResource MaterialDesignFloatingHintDatePicker}"
materialDesign:HintAssist.Hint="Birth Date" Width="400"
HorizontalAlignment="Stretch"
Text="{Binding Player.BirthDate,UpdateSourceTrigger=PropertyChanged,Mode=TwoWay}"
FontSize="14" />
</StackPanel>
<StackPanel Grid.Row="4" Orientation="Horizontal" HorizontalAlignment="Stretch" Margin="10 20 0 0">
<materialDesign:PackIcon
Kind="TshirtCrew"
Foreground="{Binding ElementName=TeamCombo, Path=BorderBrush}"/>
<ComboBox IsEditable="True" Width="400"
Style="{StaticResource MaterialDesignFloatingHintComboBox}"
x:Name="TeamCombo" DisplayMemberPath="Name"
ItemsSource="{Binding Teams}" SelectedValue="{Binding Player.Team}"
materialDesign:HintAssist.Hint="Team"/>
</StackPanel>
<StackPanel Grid.Row="5" Orientation="Horizontal" HorizontalAlignment="Stretch" Margin="10 20 0 0">
<materialDesign:PackIcon
Kind="Soccer" Foreground="{Binding ElementName=GoalsTextBox, Path=BorderBrush}"/>
<TextBox Width="400"
x:Name="GoalsTextBox"
materialDesign:HintAssist.Hint="Goals"
Style="{StaticResource MaterialDesignFloatingHintTextBox}">
<TextBox.Text>
<Binding Path="Player.GoalsNumber" UpdateSourceTrigger="PropertyChanged" Mode="TwoWay"/>
</TextBox.Text>
</TextBox>
</StackPanel>
<Button Grid.Row="6" HorizontalAlignment="Center" Margin="5" Style="{StaticResource MaterialDesignOutlinedButton}" ToolTip="Add a player"
Content="Add" Width="200" Command="{Binding AddPlayerCommand}" />
</Grid>
</UserControl>
And here is the viewModel of the current View.
Let's analyze this viewModel.
First thing we noteis that it extends from AviewModel, which is an abstract class that we created in the solution ( you will find it in the sample)
It contains only the definition of the method NotifyPropertyChanged. This method allows the source (which is the property Player) to notify the view (the .xaml) about the updates that were done in the viewModel
Without the notifyPropertyChanged the view won't be changed if anything is updated in the viewModel side. This is also noted in the xaml by defining the mode of binding (TwoWay: from view to viewModel and from ViewModel to the view)
public class AddPlayersViewModel : AViewModel
{
private Player _player;
public Player Player
{ get { return _player; } set { _player = value; NotifyPropertyChanged("Player"); } }
public List<Player> Players { get; set; }
public List<Team> Teams { get; set; }
public ICommand AddPlayerCommand { get; set; }
public AddPlayersViewModel()
{
Players= DataProvider.DataProvider.Instance.GetPlayers();
Teams= DataProvider.DataProvider.Instance.GetTeams();
AddPlayerCommand = new DelegateCommand(() => AddPlayer(),()=> CanAddPlayer());
Player = new Player();
}
private void AddPlayer()
{
DataProvider.DataProvider.Instance.AddPlayer(Player);
MessageBox.Show("The Player was added successfully !");
Player = new Player();
}
public bool CanAddPlayer()
{
return !string.IsNullOrWhiteSpace(Player.FirstName);
}
}
In our solution, there are two others views: The second one is the list of the players and the third one is a dashboard. Their principle of binding is the same as this first one.
Only the specificity of the item changes (like the dataGrid), you can find them directly in the sample. But if there is anything which is not clear in the sample, feel free to contact me or comment the Post.
Now we created all the views with their viewModels, we need to call them in the mainWindow.xam and ensure that the navigation is done correctly.
For this case, we need the main Window and it's as follows :
In the main window, we created a Grid and we ensured that it's separated into two regions: a header and a content.
The header is the first row of the grid that contains the title, version ...
The Content is a tabControl: The tabControl contains several TabItem and under every TabItem we call the view that we need (AddPlayer; GetListofPlayers and the dashboard)
<Window x:Class="SoccerManagement.Main.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
mc:Ignorable="d"
xmlns:views="clr-namespace:SoccerManagement.Main.Views" WindowStyle="None"
Title="MainWindow" Height="600" Width="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="70"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<!--The header of the application that remains unchanged while navigating-->
<Grid Grid.Row="0" Background="#320b86">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"></ColumnDefinition>
<ColumnDefinition Width="4*"></ColumnDefinition>
<ColumnDefinition Width="Auto"></ColumnDefinition>
<ColumnDefinition Width="0.75*"></ColumnDefinition>
<ColumnDefinition Width="Auto"></ColumnDefinition>
<ColumnDefinition Width="Auto"></ColumnDefinition>
</Grid.ColumnDefinitions>
<StackPanel Margin="20 10 0 0" Grid.Row="0" Grid.Column="0" Orientation="Vertical">
<TextBlock Text="Soccer Federation" HorizontalAlignment="Center" FontSize="15" Foreground="White"></TextBlock>
<TextBlock Text="version 1.0" HorizontalAlignment="Center" FontSize="15" Foreground="White"></TextBlock>
</StackPanel>
<TextBlock Text="Player Management" VerticalAlignment="Center" HorizontalAlignment="Center" FontWeight="Bold" FontSize="24" Foreground="White" Grid.Row="0" Grid.Column="1"></TextBlock>
<Button Grid.Column="4" Width="50" Height="50" Margin="0" BorderBrush="Transparent" BorderThickness="100" Click="Button_Click">
<materialDesign:PackIcon
Width="20"
Height="20"
Foreground="white"
Kind="WindowMinimize" />
</Button>
<Button Grid.Column="5" Width="50" Height="50" Margin="0" BorderBrush="Transparent" BorderThickness="100" Click="Button_Click_1">
<materialDesign:PackIcon Width="20" Height="20" Foreground="white" Kind="Shutdown" />
</Button>
</Grid>
<!--The tab controls that will hold the content of the different pages -->
<TabControl Grid.Row="1" VerticalContentAlignment="Top"
Style="{StaticResource MaterialDesignNavigatilRailTabControl}"
materialDesign:ColorZoneAssist.Mode="Standard" >
<TabItem Width="140" Height="140" x:Name="AddPlayerTab">
<TabItem.Header>
<StackPanel MouseLeftButtonDown="AddPlayerStackPanel_MouseRightButtonDown" Background="WhiteSmoke" Height="140" Width="140" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" >
<materialDesign:PackIcon Kind="AccountsAdd" Width="30" Height="30" Margin="0 10 0 0" VerticalAlignment="Center" HorizontalAlignment="Center"/>
<TextBlock Text="Add a Player" HorizontalAlignment="Center"/>
</StackPanel>
</TabItem.Header>
<views:AddPlayerView x:Name="AddPlayerViewTab"></views:AddPlayerView>
</TabItem>
<TabItem Width="140" Height="140" x:Name="GetPlayersTab">
<TabItem.Header>
<StackPanel Background="WhiteSmoke" MouseLeftButtonDown="GetPlayerStackPanel_MouseRightButtonDown" Height="140" Width="140" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<materialDesign:PackIcon Kind="AccountMultiple" Width="30" Height="30" Margin="0 10 0 0" VerticalAlignment="Center" HorizontalAlignment="Center"/>
<TextBlock Text="List Of players" HorizontalAlignment="Center"/>
</StackPanel>
</TabItem.Header>
<views:ListOfPlayersView x:Name="ListOfPlayerViewTab"></views:ListOfPlayersView>
</TabItem>
<TabItem Width="140" Height="140" x:Name="DashboardTab">
<TabItem.Header>
<StackPanel Background="WhiteSmoke" MouseLeftButtonDown="GetDashboardStackPanel_MouseRightButtonDown" Height="140" Width="140" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<materialDesign:PackIcon Kind="ChartBar" Width="30" Height="30" Margin="0 10 0 0" VerticalAlignment="Center" HorizontalAlignment="Center"/>
<TextBlock Text="Dashboard" HorizontalAlignment="Center"/>
</StackPanel>
</TabItem.Header>
<views:DashboardView></views:DashboardView>
</TabItem>
</TabControl>
</Grid>
</Window>
There is something that we need to add in the code behind (MainWindow.xaml.cs) since the TabItem doesn't refresh its view from the viewModel while the navigation between tabs.
This is why we ensured that when the user click on the tab, it will certainly click on the StackPanel created in the tabItem. And then we define the action to do in the event MouseLeftButtonDown.
So the MainWindow.xaml.cs looks like :
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
WindowState = WindowState.Minimized;
}
private void Button_Click_1(object sender, RoutedEventArgs e)
{
Application.Current.Shutdown();
}
private void AddPlayerStackPanel_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
AddPlayerTab.Content = new AddPlayerView();
}
private void GetPlayerStackPanel_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
GetPlayersTab.Content = new ListOfPlayersView();
}
private void GetDashboardStackPanel_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
DashboardTab.Content = new DashboardView();
}
}
Now, you can execute the application and enjoy your simple navigation between the screens in the same way you navigate in a website.
Remarks
- In sample you will find some classes related to the infrastructure like the :
- DelegateCommand ( The implementation of the ICommand: the action raised while clicking on a button)
- The AviewModel that implements the interface INotifyPropertyChanged.
- Some update were done in app.xaml to be able to use material design correctly, you will find them in the sample
Conclusion
The way of navigation between screens varies from one application to another. It depends on the nature of the application and the need of the user.
So the solution of using the TabControls, is one among several other ways to do it.
You can download the code directly from here or from my git: https://github.com/Aymenamr/WPF-Sample-using-TabControl-netcore-6
If you have any questions or remarks, feel free to contact me.
Happy coding