Я делаю вспомогательную программу для построения статистических графиков. Я полный новичок в WPF. У меня уже есть 2 ListBoxes:
Сначала я хочу выбрать (выбрать) профиль из первого списка. При этом выборе список RaceFolders должен заполниться сам. Затем я хочу выбрать RaceFolder. После его выбора программа должна составить мне сюжет. (Что само по себе не важно, но по сути это повторение проблемы с выбором списка профилей, только более сложное)
В проекте есть несколько файлов, но те, которые, по моему мнению, важны для решения проблемы:
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>
У меня проблема с этими двумя списками в конце.
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();
}
}
}
Шеллвиевмодел.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());
}
}
}
Кажется, что свойства привязаны только к ShellViewModel из-за DataContext в верхней части файла xaml. Однако когда я пытаюсь использовать функцию «SelectionChanged», она всегда сопоставляет ее с ShellView.
Это означает, что я не могу использовать свои привязанные свойства. Я также уже пытался извлечь его из отправителя, но по какой-то причине он продолжал получать «ноль».
(Я следил за этим: https://begincodingnow.com/wpf-listbox-selection/)
Я также был бы признателен, если бы вы указали мне на хорошие ресурсы, которые подробно объясняют такие ситуации. В WPF в MVVM очень мало действительно хороших руководств и инструкций. По крайней мере, я не нашел многого об этом.
Когда вы регистрируете обработчик событий в XAML, он должен находиться в том же частичном классе. Имя обработчика событий не может указывать на другой класс. Чтобы позволить другому классу обрабатывать событие, он должен подписаться на это событие, как обычно.
Поскольку вы уже привязываете ListBox.SelectedItem
к классу модели представления, вы можете запустить необходимую операцию из установщика свойств источника привязки.
Свойства в вашем ShellView
кажутся избыточными. Кажется, они отражают свойства ShellViewModel
. Однако если вы хотите привязаться к этим свойствам, их следует реализовать как свойства зависимости.
Ваш ShellViewModel
должен быть реализован INotifyPropertyChanged
(даже если стоимость недвижимости на самом деле не изменится). Как только вы привязываетесь к свойству, оно должно быть свойством зависимости, если источником является DependencyObject
. Если источник не является DependencyObject
, то он должен реализовать INotifyPropertyChanged и вызвать событие INotifyPropertyChanged.PropertyChanged
из установщика свойства. Хотя привязки будут работать без реализации этого в интерфейсе, вы создадите утечку памяти.
См. Обзор привязки данных (WPF .NET), чтобы узнать больше.
Объект, возвращаемый из ListBox.SelectedItem
, — это элемент данных, заполняющий ListBox
. Это всего лишь ListBoxItem
, в который явно добавлены экземпляры ListBoxItem
к ListBox.ItemsSource
. Интересно, что ваши обработчики событий ListBox.SelectionChanged
не выдают недопустимые исключения приведения типов.
Асинхронные методы, подобные вашему ActivateItemAsync
, следует ожидать при использовании await
. В противном случае поведение непредсказуемо. Каждый метод, ожидающий асинхронного метода, должен быть объявлен как async Task
или async Task<TResult>
. вызывающая сторона этого метода также должна его ожидать.
Наконец, вы не определили ни одного DataContext
для своего представления. Установка контекста данных времени разработки — это всего лишь контекст данных времени разработки. Это не контекст данных времени выполнения. Сотекст данных времени разработки предназначен для того, чтобы помочь вам работать в конструкторе XAML и позволить конструктору XAML обеспечить предварительный просмотр.
<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>
Улучшенная и исправленная версия вашего кода может выглядеть следующим образом:
Шеллвиев.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>
Шеллвиевмодел.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));
}
Кроме того, у меня есть ошибка CS0079 на PropertyChanged
в OnPropertyChanged
(Learn.microsoft.com/en-us/dotnet/csharp/misc/…). Я проверил версию .Net, и это предположительно .Net 6.0. Что должно быть хорошо. Знаете ли вы, что происходит?
Вы ждете асинхронные методы? await ActivateItemAsync()
. Кстати, в XAML нельзя использовать функции (методы), если они не являются обработчиками событий. Я думаю, у тебя что-то другое?
Теперь я вижу, что вы используете фреймворк Caliburn. Вы должны сказать об этом в своем вопросе и даже об этом в своем вопросе соответственно. Не думайте, что все используют эту структуру. Это не стандарт де-факто.
О, теперь я понял - я что-то напутал, извините. Однако у меня все еще есть эта странная ошибка CS0079 — я попробую сделать это старым способом. Если вы, возможно, не знаете, что является причиной этого?
Вы используете мой опубликованный код? Правильно ли определено событие PropertyChanged? Это должно быть публичное поле.
Где именно эта ошибка, в какой строке?
Теперь и хорошо - я допустил орфографическую ошибку в определении и почему-то показывалась именно такая ошибка. Теперь я пытаюсь довести дело до конца. Спасибо - Вы очень помогли. :)
По какой-то странной причине неасинхронная версия этой функции не работает. :( (stackoverflow.com/questions/69062707/…) Поскольку я использую эту функцию в xaml, знаете ли вы, где/как я могу ее дождаться?