I'm making a statistic-plotting auxiliary program. I'm a complete Newbie in WPF. I already have 2 ListBoxes:
First I want to choose (Select) a profile form the first ListBox. On that selection RaceFolders List should populate itself. Then I want to choose a RaceFolder. Upon it's selection program should make me a plot. (Which is not important by itself, but it is defacto a reiteration of the problem with Profile List selection, but more complicated)
A have a few files in the project, but those that I think are important for the problem are :
ShellView.xaml :
<Window x:Class="RaceExplorer.Views.ShellView"
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:i="http://schemas.microsoft.com/xaml/behaviors"
xmlns:cal="http://www.caliburnproject.org"
xmlns:local="clr-namespace:RaceExplorer.Views"
xmlns:vmodels="clr-namespace:RaceExplorer.ViewModels"
xmlns:oxy="http://oxyplot.org/wpf"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance Type=vmodels:ShellViewModel}"
Title="ShellView" Height="900" Width="1600"
>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="240" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TabControl Grid.Column ="1">
<TabItem>
<TabItem.Header>
<StackPanel Orientation="Horizontal">
<!--<Image Source="/WpfTutorialSamples;component/Images/bullet_blue.png" />-->
<TextBlock Text="Blue" Foreground="Blue" />
</StackPanel>
</TabItem.Header>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="240" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="240" />
</Grid.RowDefinitions>
<Grid Grid.Row="1" Grid.Column="1">
<Grid.ColumnDefinitions>
[...]
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
[...]
</Grid.RowDefinitions>
<Button x:Name="LoadStatView">
LOAD
</Button>
</Grid>
<TextBlock Text="{Binding PropRaceData.TotalObstacles}"></TextBlock>
<ContentControl Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="5"
x:Name="ActiveItem" />
<!--<GroupBox Grid.Row="0" Grid.Column="1">
<oxy:PlotView Model ="{Binding MyModel}" Name="plot"/>
</GroupBox>-->
</Grid>
<!--<Label Content="Content goes here..." />-->
</TabItem>
<TabItem>
<TabItem.Header>
<StackPanel Orientation="Horizontal">
<!--<Image Source="/WpfTutorialSamples;component/Images/bullet_red.png" />-->
<TextBlock Text="Red" Foreground="Red" />
</StackPanel>
</TabItem.Header>
</TabItem>
<TabItem>
<TabItem.Header>
<StackPanel Orientation="Horizontal">
<!--<Image Source="/WpfTutorialSamples;component/Images/bullet_green.png" />-->
<TextBlock Text="Green" Foreground="Green" />
</StackPanel>
</TabItem.Header>
</TabItem>
</TabControl>
<Label Content="Data Selector"/>
<Grid Margin="0,28,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="24" Name="profileListTitle" />
<RowDefinition Height="240" Name="profileList" />
<RowDefinition Height="24" Name="RaceListTitle" />
<RowDefinition Height="240" Name="RaceList" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock>Select Profile :</TextBlock>
<ListBox
ItemsSource="{Binding ProfileList}"
SelectedItem="{Binding SelectedProfile, Mode=TwoWay}"
SelectionChanged="ProfileListBox_SelectionChanged"
ScrollViewer.VerticalScrollBarVisibility="Visible"
Grid.Row="1" >
</ListBox>
<TextBlock Grid.Row="2">Select Race :</TextBlock>
<ListBox
ItemsSource="{Binding RacesCollection}"
SelectedItem="{Binding SelectedRaceFolder, Mode=TwoWay}"
SelectionChanged="RaceListBox_SelectionChanged"
ScrollViewer.VerticalScrollBarVisibility="Visible"
Grid.Row="3"
Name ="RaceListBox">
</ListBox>
</Grid>
</Grid>
I have a problem with those two listboxes on the end.
ShellView.cs:
using RaceExplorer.Models;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using RaceExplorer.ViewModels;
namespace RaceExplorer.Views
{
/// <summary>
/// Logika interakcji dla klasy ShellView.xaml
/// </summary>
public partial class ShellView : Window
{
private string _totalRaceObstacles = "TOTAL";
public string TotalRaceObstacles
{
//get { return raceData.TotalObstacles.ToString();}
get { return _totalRaceObstacles; }
}
private string _selectedProfile;
public string SelectedProfile
{
get { return _selectedProfile; }
set { _selectedProfile = value; }
}
private string _selectedRaceFolder;
public string SelectedRaceFolder
{
get { return _selectedRaceFolder; }
set { _selectedRaceFolder = value; }
}
public ShellView()
{
InitializeComponent();
}
private void RaceListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
ListBox lb = sender as ListBox;
ListBoxItem lbi = lb.SelectedItem as ListBoxItem;
TextBlock tb = (TextBlock)lbi.Content;
SelectedRaceFolder = tb.Text;
_ = SelectedRaceFolder == null ? ExplorerPath.profileChildName = "" : ExplorerPath.profileChildName = SelectedRaceFolder;
ExplorerPath.updatePath();
}
private void ProfileListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
ListBox lb = sender as ListBox;
//ListBoxItem lbi = lb.SelectedItem as ListBoxItem;
//TextBlock tb = (TextBlock)lbi.Content;
SelectedProfile = lb.SelectedItem.ToString();
_ = SelectedProfile == null ? ExplorerPath.profileName = "" : ExplorerPath.profileName = SelectedProfile;
ExplorerPath.updatePath();
}
}
}
ShellViewModel.cs
using Caliburn.Micro;
using RaceExplorer.Models;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Automation;
namespace RaceExplorer.ViewModels
{
public class ShellViewModel: Conductor<object>
{
private int _firstName;
public int FirstName
{
get { return _firstName; }
set { _firstName = value; }
}
private int _profileListData;
public int ProfileListItem
{
get { return _profileListData; }
set { _profileListData = value; }
}
private ObservableCollection<string> _profileList = new ObservableCollection<string>();
public ObservableCollection<string> ProfileList
{
get { return _profileList; }
}
string testChartPath = @"[...]";
private RaceData raceData = new RaceData();
public RaceData PropRaceData
{
get { return raceData = new RaceData(); }
set { raceData = value; }
}
private ObservableCollection<string> _racesCollection = new ObservableCollection<string>();
public ObservableCollection<string> RacesCollection
{
get { return _racesCollection; }
set { _racesCollection = value; }
}
//private string _selectedProfile;
//public string SelectedProfile
//{
// get { return _selectedProfile; }
// set { _selectedProfile = value; }
//}
//private string _selectedRaceFolder;
//public string SelectedRaceFolder
//{
// get { return _selectedRaceFolder; }
// set { _selectedRaceFolder = value; }
//}
public ShellViewModel()
{
_profileList = getProfileNames();
raceData.getDataFrom(testChartPath);
_racesCollection = getRaceDirsInProfile();
}
public ObservableCollection<string> getProfileNames()
{
string profilePath = @"[...]";
ObservableCollection<string> profileList = new ObservableCollection<string>();
DirectoryInfo directoryInfo = new DirectoryInfo(profilePath);
FileInfo[] profileFiles = directoryInfo.GetFiles();
profileList.Clear();
foreach (FileInfo fileInfo in profileFiles)
{
if (fileInfo.Extension == ".profile")
{
profileList.Add(fileInfo.Name.Split(".")[0]);
}
}
return profileList;
}
public ObservableCollection<string> getRaceDirsInProfile()
{
if (ExplorerPath.profileName == "")
return null;
ExplorerPath.updatePath();
string profilePath = @"[...]";
ObservableCollection<string> dirList = new ObservableCollection<string>();
DirectoryInfo directoryInfo = new DirectoryInfo(ExplorerPath.profilePath);
DirectoryInfo[] raceFiles = directoryInfo.GetDirectories();
dirList.Clear();
foreach (DirectoryInfo dirInfo in raceFiles)
{
dirList.Add(dirInfo.Name.Split(".")[1]);
}
return dirList;
}
public void LoadStatView()
{
//base.ActivateItem(new StatViewModel());
ActivateItemAsync(new StatViewModel());
}
}
}
The properties seem to be Binded only to ShellViewModel because of the DataContext on the top of xaml file. However when I try to use "SelectionChanged" functionality it always maps it to ShellView.
Which means I can't use my Binded properties. I also tried to extract it from sender already, but it kept receiving "null" for some reason.
(I followed this : https://begincodingnow.com/wpf-listbox-selection/)
I would also appreciate if you could point me to some good resources that extensively explain such situations. WPF in MVVM has very few actually good tutorials or how to's. At least I didn't find much about it.
When you register event handler in XAML, then the event handler must be in the same partial class. The event handler name can't point to a different class. To allow a different class to handle the event, it must subscribe to this event as usual.
Because you already bind the ListBox.SelectedItem
to your view model class, you can trigger the required operation from the property setter of the binding source.
The properties in your ShellView
seem to be redundant. They seem to mirror the ShellViewModel
properties. However, if you want to bind to this properties, then they should be implemented as dependency properties.
Your ShellViewModel
must implement INotifyPropertyChanged
(even if property value actually won't change). As soonn as you bind to a property the property must be a dependency property if the source is a DependencyObject
. If the source i s not a DependencyObject
then it must implement INotifyPropertyChanged
and raise the INotifyPropertyChanged.PropertyChanged
event from the property setter. Although bindings would work without implementing that in terface, you would create a memory leak.
See Data binding overview (WPF .NET) to learn more.
The object returned from the ListBox.SelectedItem
is the data item that populates the ListBox
. It's only a ListBoxItem
is have explicitly added ListBoxItem
instances to the ListBox.ItemsSource
. I wonder that your ListBox.SelectionChanged
event handlers don't throw invalid cast exceptions.
Asynchronous m ethods like your ActivateItemAsync
must be awaited using await
. Oterwise the behavior is unpredicatble. Every method that awaits an asynchronous method must i tself declared as async Task
or async Task<TResult>
. the caller of this method must also await it.
Finally, you have not defined any DataContext
for your view. Setting a designtime data context, is just that: a designtime data context. It's not a runtime datat context. Design time data cotext is meant to help you to work in the XAML Designer and to enable the XAML Designer to provide a preview.
<Window
<!--
This alone will lead to broken bindings that use the DataContext as source
because the designtime context ios not available during runtime.
-->
d:DataContext="{d:DesignInstance Type=vmodels:ShellViewModel}">
<!--
Define the real DataContext either in XAML (below)
or in C# (code-behind) e.g. in the constructor.
But don't define it in XAML AND C#! Choose one.
-->
<Window.DataContext>
<vmodels:ShellViewModel />
</Window.DataContext>
</Window>
An improved and fixed version of your code could look as folows:
ShellView.xaml.cs
public partial class ShellView : Window
{
public ShellView()
{
this.DataContext = new ShellViewModel();
InitializeComponent();
}
}
ShelView.xaml
<Window>
<Stackpanel>
<TextBlock Text="Select Profile:" />
<ListBox ItemsSource="{Binding ProfileList}"
SelectedItem="{Binding SelectedProfile}" />
<TextBlock Text="Select Race:" />
<ListBox ItemsSource="{Binding RacesCollection}"
SelectedItem="{Binding SelectedRaceFolder}" />
</StackPanel>
</Window>
ShellViewModel.cs
public class ShellViewModel : Conductor<object>, INotifyPropertyChanged
{
private int _firstName;
public int FirstName
{
get => _firstName;
set
{
_firstName = value;
OnPropertyChanged();
}
}
private int _profileListData;
public int ProfileListItem
{
get => _profileListData;
set
{
_profileListData = value;
}
}
string testChartPath = @"[...]";
private RaceData raceData;
public RaceData PropRaceData
{
get => raceData;
set
{
raceData = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
public ObservableCollection<string> ProfileList { get; }
public ObservableCollection<string> RacesCollection { get; }
private string _selectedProfile;
public string SelectedProfile
{
get => _selectedProfile;
set
{
_selectedProfile = value;
OnPropertyChanged();
//Do something on selection changed e.g. populate races folder collection
OnSelectedProfileChanged();
}
}
private string _selectedRaceFolder;
public string SelectedRaceFolder
{
get => _selectedRaceFolder;
set
{
_selectedRaceFolder = value;
OnPropertyChanged();
// Do something oin selection changed e.g. plot
OnSelectedRaceFolder();
}
}
public ShellViewModel()
{
this.PprofileList = GetProfileNames();
raceData = new RaceData();
raceData.GetDataFrom(testChartPath);
this.RacesCollection = GetRaceDirsInProfile();
}
protected virtual void OnSelectedProfileChanged()
{
// Don't replace the ObservableCollection.
// Instead clear it and add new items to improve the UI performance
CreatedRacesFolder();
}
protected virtual void OnSelectedRaceFolder()
{
StartPlot();
}
// C# use PascalCase naming for methods and properties or static const fields.
public ObservableCollection<string> GetProfileNames()
{
string profilePath = @"[...]";
ObservableCollection<string> profileList = new ObservableCollection<string>();
DirectoryInfo directoryInfo = new DirectoryInfo(profilePath);
// Don't use DirectoryInfo.GetFiles as this will lead to two complete iteration in your case.
// Prefer DirectoryInfo.EnumerateFiles. In addition to returning a deferred enumeration (enumerator)
// EnumerateFiles it allows you to abort the enumeartion at any time
// while GetFiles will always force you to enumerate all files.
IEnumerable<FileInfo> profileFiles = directoryInfo.EnumerateFiles();
foreach (FileInfo fileInfo in profileFiles)
{
if (fileInfo.Extension == ".profile")
{
// Avoid string.operation and use Path Helper API to improve readability
Path.GetFileNameWithoutExtension(fileInfo.Name);
}
}
return profileList;
}
// C# use PascalCase naming for methods and properties or static const fields.
public ObservableCollection<string> GetRaceDirsInProfile()
{
// C# use PascalCase naming for methods and properties or static const fields.
if (ExplorerPath.ProfileName == "")
{
// Return an empty collection instead of NULL
return new ObservableCollection<string>();
}
// C# use PascalCase naming for methods and properties or static const fields.
ExplorerPath.UpdatePath();
string profilePath = @"[...]";
ObservableCollection<string> dirList = new ObservableCollection<string>();
DirectoryInfo directoryInfo = new DirectoryInfo(ExplorerPath.profilePath);
// Don't use DirectoryInfo.GetDirectories as this will lead to two complete iteration in your case.
// Prefer DirectoryInfo.EnumerateDirectories. In addition to returning a deferred enumeration (enumerator)
// EnumerateDirectories it allows you to abort the enumeartion at any time
// while GetDirectories will always force you to enumerate all directories.
IEnumerable<DirectoryInfo> raceFolders = directoryInfo.EnumerateDirectories();
foreach (DirectoryInfo directoryInfo in raceFolders)
{
dirList.Add(dirInfo.Name.Split(".")[1]);
}
return dirList;
}
public async Task LoadStatViewAsync()
{
//base.ActivateItem(new StatViewModel());
// Why is this async method not awaited?
// The method should be 'public async Task LoadStartViewAsync()'.
// The caller of this method must also 'await LoadStartViewAsync()'
await ActivateItemAsync(new StatViewModel());
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
=> this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}