WPF - 如何使 ListBox 在 MVVM 中“选择时”运行函数?

问题描述 投票:0回答:1

我正在制作一个统计绘图辅助程序。我是 WPF 的完全新手。 我已经有 2 个列表框了:

  • 个人资料列表
  • 比赛文件夹列表

首先我想从第一个列表框中选择(选择)一个配置文件。在该选择上,RaceFolders 列表应自行填充。然后我想选择一个RaceFolder。根据它的选择程序应该让我有一个情节。 (这本身并不重要,但它实际上是对配置文件列表选择问题的重申,但更复杂)

项目中有一些文件,但我认为对问题很重要的文件是:

  • ShellView.cs
  • ShellViewModel.cs
  • ShellView.xaml

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();
        }

    }
}

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());
        }


    }
}

由于 xaml 文件顶部的 DataContext,这些属性似乎仅绑定到 ShellViewModel。然而,当我尝试使用“SelectionChanged”功能时,它总是将其映射到 ShellView。 这意味着我无法使用我的绑定属性。我也尝试从发件人那里提取它,但由于某种原因它一直收到“null”。
(我遵循这个:https://begincodingnow.com/wpf-listbox-selection/

如果您能为我指出一些可以广泛解释此类情况的好资源,我也将不胜感激。 MVVM 中的 WPF 几乎没有真正好的教程或操作方法。至少我没有找到太多。

c# wpf mvvm listbox
1个回答
0
投票

当您在 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>

代码的改进和修复版本可能如下所示:

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()'
    ActivateItemAsync(new StatViewModel());
  }

  protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    => this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
© www.soinside.com 2019 - 2024. All rights reserved.