Magus' Exhibitionist Code
-
Continuing the discussion from .NET (WPF) CollectionViews and Filtering:
For such scenarios, I tend to use the kind of collection rebuilding I mentioned above, usually combined with LINQ in the view model. However, your method intrigues me.
Figured I'd follow up on my promises and post a bit of my code here: my worst view and viewmodel, in the form they currently exist in my repo.
First, my viewmodel: (Note that I use AlwaysAligned, so there are tabs in places you wouldn't normally put tabs in code)
using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel.Composition; using System.Linq; using System.Windows.Input; using MeilingApp.Data.Enums; using MeilingApp.Data.Models; using MeilingApp.Editor.Tooling; using MeilingApp.Editor.ViewModels.CharacterCreation; using MeilingApp.Editor.Views.CharacterCreation; namespace MeilingApp.Editor.ViewModels { [Export] public class SkillViewModel : ViewModel { private readonly InnerNavigation navigation; private readonly Saver saver; public ICommand SaveCommand { get; private set; } public ICommand AddTechniqueCommand { get; private set; } public DelegateCommand AddSkillCommand { get; private set; } public DelegateCommand<ProjectileSet> AddProjectileCommand { get; private set; } public DelegateCommand AddProjectileSetCommand { get; private set; } public AnimationViewModel AnimationViewModel { get; private set; } public ProjectileViewModel ProjectileViewModel { get; private set; } public IEnumerable<ChargeLevel> ChargeLevels { get; private set; } public IEnumerable<OffsetSource> OffsetSources { get; private set; } private Skill selectedSkill; public Skill SelectedSkill { get { return selectedSkill; } set { if (selectedSkill == value) return; selectedSkill = value; RaisePropertyChanged(); } } private Technique selectedTechnique; public Technique SelectedTechnique { get { return selectedTechnique; } set { if (selectedTechnique == value) return; selectedTechnique = value; RaisePropertyChanged(); } } private ObservableCollection<Technique> techniques; public ObservableCollection<Technique> Techniques { get { return techniques; } set { if (techniques == value) return; techniques = value; RaisePropertyChanged(); } } [ImportingConstructor] public SkillViewModel ( [Import] InnerNavigation navigation, [Import] Saver saver, [Import] AnimationViewModel animationViewModel, [Import] ProjectileViewModel projectileViewModel ) { SaveCommand = new DelegateCommand(Save); AddSkillCommand = new DelegateCommand(AddSkill, CanAddSkill); AddTechniqueCommand = new DelegateCommand(AddTechnique); AddProjectileCommand = new DelegateCommand<ProjectileSet>(AddProjectile, CanAddProjectile); AddProjectileSetCommand = new DelegateCommand(AddProjectileSet, CanAddProjectileSet); Techniques = new ObservableCollection<Technique>(); ChargeLevels = Enum.GetValues(typeof (ChargeLevel)).Cast<ChargeLevel>(); OffsetSources = Enum.GetValues(typeof (OffsetSource)).Cast<OffsetSource>(); this.navigation = navigation; this.saver = saver; AnimationViewModel = animationViewModel; ProjectileViewModel = projectileViewModel; } private void Save() { navigation.NavigateTo<CharacterSelectView>(); saver.Save(); } private void AddSkill() { SelectedSkill = new Skill { Launches = new ObservableCollection<ProjectileSet>() }; SelectedTechnique.Skills.Add(SelectedSkill); AddProjectileSetCommand.RaiseCanExecuteChanged(); } private bool CanAddSkill() { return SelectedTechnique != null; } private void AddTechnique() { SelectedTechnique = new Technique { Skills = new ObservableCollection<Skill>() }; Techniques.Add(SelectedTechnique); AddSkillCommand.RaiseCanExecuteChanged(); } private void AddProjectileSet() { SelectedSkill.Launches.Add(new ProjectileSet { Projectiles = new ObservableCollection<ProjectileData>() }); AddProjectileCommand.RaiseCanExecuteChanged(); } private bool CanAddProjectileSet() { return SelectedSkill != null; } private void AddProjectile(ProjectileSet projectileSet) { projectileSet.Projectiles.Add(new ProjectileData { ProjectileOrigin = new ProjectileOrigin() }); } private bool CanAddProjectile(ProjectileSet projectileSet) { return projectileSet != null; } } }
Next is my view's codebehind, to expose the meffing glory of it all:
using System.ComponentModel.Composition; using MeilingApp.Editor.Attributes; using MeilingApp.Editor.ViewModels; namespace MeilingApp.Editor.Views { /// <summary> /// Interaction logic for SkillView.xaml /// </summary> [InnerNavigation(typeof(CharacterCreationView))] public partial class SkillView : IView { [Import(typeof(SkillViewModel))] public ViewModel ViewModel { get { return DataContext as SkillViewModel; } set { DataContext = value; } } public SkillView() { InitializeComponent(); } } }
And then the worst part, the view itself:
<UserControl x:Class="MeilingApp.Editor.Views.SkillView" 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:viewModels="clr-namespace:MeilingApp.Editor.ViewModels" xmlns:models="clr-namespace:MeilingApp.Data.Models;assembly=MeilingApp.Data" xmlns:views="clr-namespace:MeilingApp.Editor.Views" mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="300" d:DataContext="{d:DesignInstance viewModels:SkillViewModel}" > <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="1*"/> <ColumnDefinition Width="5"/> <ColumnDefinition Width="5*"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="*"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <ListBox Grid.Column="0" Grid.Row="0" ItemsSource="{Binding Techniques}" SelectedItem="{Binding SelectedTechnique}" DisplayMemberPath="Name"/> <Button Grid.Column="0" Grid.Row="1" HorizontalAlignment="Right" Content="New Technique" Command="{Binding AddTechniqueCommand}"/> <Grid Grid.Column="2" Grid.Row="0"> <Grid.Style> <Style TargetType="{x:Type Grid}"> <Setter Property="IsEnabled" Value="True"/> <Style.Triggers> <DataTrigger Binding="{Binding SelectedTechnique}" Value="{x:Null}"> <Setter Property="IsEnabled" Value="False"/> </DataTrigger> </Style.Triggers> </Style> </Grid.Style> <Grid.ColumnDefinitions> <ColumnDefinition Width="1*"/> <ColumnDefinition Width="5"/> <ColumnDefinition Width="5*"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="5"/> <RowDefinition Height="*"/> <RowDefinition Height="5"/> </Grid.RowDefinitions> <TextBlock Grid.Column="0" Grid.Row="0" Text="Name:" HorizontalAlignment="Right"/> <TextBox Grid.Column="2" Grid.Row="0" Text="{Binding SelectedTechnique.Name}"/> <Grid Grid.Column="0" Grid.Row="2"> <Grid.RowDefinitions> <RowDefinition Height="*"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <ListBox Grid.Row="0" SelectedItem="{Binding SelectedSkill}" ItemsSource="{Binding SelectedTechnique.Skills}"/> <Button Grid.Row="1" HorizontalAlignment="Right" Content="New Skill" Command="{Binding AddSkillCommand}"/> </Grid> <Grid Grid.Column="2" Grid.Row="2"> <Grid.Style> <Style TargetType="{x:Type Grid}"> <Setter Property="IsEnabled" Value="True"/> <Style.Triggers> <DataTrigger Binding="{Binding SelectedSkill}" Value="{x:Null}"> <Setter Property="IsEnabled" Value="False"/> </DataTrigger> </Style.Triggers> </Style> </Grid.Style> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="5"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <Grid.Resources> <Style TargetType="{x:Type TextBlock}" BasedOn="{StaticResource {x:Type TextBlock}}"> <Setter Property="HorizontalAlignment" Value="Right"/> <Setter Property="VerticalAlignment" Value="Center"/> </Style> <Style TargetType="{x:Type ComboBox}" BasedOn="{StaticResource {x:Type ComboBox}}"> <Setter Property="Margin" Value="0 2"/> </Style> <Style TargetType="{x:Type TextBox}" BasedOn="{StaticResource {x:Type TextBox}}"> <Setter Property="Margin" Value="0 2"/> </Style> </Grid.Resources> <TextBlock Grid.Column="0" Grid.Row="1" Text="Charge Level:"/> <ComboBox Grid.Column="2" Grid.Row="1" ItemsSource="{Binding ChargeLevels}" SelectedItem="{Binding SelectedSkill.MaximumCharge}"/> <TextBlock Grid.Column="0" Grid.Row="2" Text="Projectile Sets:"/> <ItemsControl Grid.Column="2" Grid.Row="2" Grid.RowSpan="2" ItemsSource="{Binding SelectedSkill.Launches}" > <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <StackPanel Orientation="Vertical"/> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> <ItemsControl.ItemTemplate> <ItemContainerTemplate> <Expander d:DataContext="{d:DesignInstance models:ProjectileSet}"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="1*"/> <ColumnDefinition Width="5"/> <ColumnDefinition Width="5*"/> </Grid.ColumnDefinitions> <TextBlock Grid.Column="0" Text="Release Frame:" HorizontalAlignment="Right" VerticalAlignment="Center" Foreground="{DynamicResource WindowText}"/> <TextBox Grid.Column="2" Text="{Binding ReleaseFrame}"/> </Grid> <ItemsControl Grid.Row="1" ItemsSource="{Binding Projectiles}"> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <WrapPanel Orientation="Horizontal"/> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> <ItemsControl.ItemTemplate> <ItemContainerTemplate> <Grid Background="{DynamicResource ButtonBackground}" Margin="2" Width="180" d:DataContext="{d:DesignInstance models:ProjectileData}"> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="5"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <Grid.Resources> <Style TargetType="{x:Type TextBlock}" BasedOn="{StaticResource {x:Type TextBlock}}"> <Setter Property="HorizontalAlignment" Value="Right"/> <Setter Property="VerticalAlignment" Value="Center"/> <Setter Property="Foreground" Value="{DynamicResource WindowText}"/> </Style> </Grid.Resources> <TextBlock Grid.Column="0" Grid.Row="0" Text="Offset From:"/> <ComboBox Grid.Column="2" Grid.Row="0" SelectedItem="{Binding ProjectileOrigin.OffsetFrom}" ItemsSource="{Binding RelativeSource={RelativeSource AncestorType={x:Type views:SkillView}}, Path=ViewModel.OffsetSources}" /> <TextBlock Grid.Column="0" Grid.Row="1" Text="X:"/> <TextBox Grid.Column="2" Grid.Row="1" Text="{Binding ProjectileOrigin.X}"/> <TextBlock Grid.Column="0" Grid.Row="2" Text="Y:"/> <TextBox Grid.Column="2" Grid.Row="2" Text="{Binding ProjectileOrigin.Y}"/> <TextBlock Grid.Column="0" Grid.Row="3" Text="Projectile:"/> <ComboBox Grid.Column="2" Grid.Row="3" ItemsSource="{Binding RelativeSource={RelativeSource AncestorType={x:Type views:SkillView}}, Path=ViewModel.ProjectileViewModel.Projectiles}" SelectedItem="{Binding Projectile}" DisplayMemberPath="Name" /> </Grid> </ItemContainerTemplate> </ItemsControl.ItemTemplate> </ItemsControl> <Button Grid.Row="2" Content="Add Projectile" Command="{Binding RelativeSource={RelativeSource AncestorType={x:Type views:SkillView}}, Path=ViewModel.AddProjectileCommand}" CommandParameter="{Binding}" HorizontalAlignment="Right"/> </Grid> </Expander> </ItemContainerTemplate> </ItemsControl.ItemTemplate> </ItemsControl> <Button Grid.Column="2" Grid.Row="4" HorizontalAlignment="Right" Content="Add Projectile Set" Command="{Binding AddProjectileSetCommand}"/> </Grid> </Grid> <Button Grid.Column="2" Grid.Row="1" HorizontalAlignment="Right" Content="Save" Command="{Binding SaveCommand}"/> </Grid> </UserControl>
If you don't use MEF, and therefore don't understand something here, just know that it puts exports into imports when it's all initialized.
-
As for why the title is what it is, I was fairly certain that if I named it "Magus' Code Exhibition", this is how it would end up anyway.
-
Out of curiosity, is Techniques set in any other place than the ctor?
-
Not yet. Once I get it to read from a saved configuration, it will. Because the observable collection property itself notifies on change, replacing the collection with a different one will update the UI, as will adding/removing to/from it.
Of particular note are the skills binding and the launches binding: they update automatically.
-
Because the observable collection property itself notifies on change, replacing the collection with a different one will update the UI, as will adding/removing to/from it.
You mean, by changing the reference on techniques?
-
Yes, if I were to assign it to
null
or to a differentObservableCollection<Technique>
, the UI will update. It seems obvious, but it's easy to forget: "I'm using an observable collection, so I should know when it changes!" - but while you do, you don't know when you're given a new one unless you notify there too. It's the only way it can be.I have a fairly awesome situation here, because I've been writing the UI and models together, and will base my XML on my models.
Still really enjoying MEF; using it for my game side and my editor.
-
Of particular note are the skills binding and the launches binding: they update automatically.
Yeah, I see how that's done and it turns out I did not understand you initially, since this is a technique I have in fact used before (I thought the Property in question was itself a collection).
-
But for other situations, like if I were to add:
public int TotalDamage { return SelectedTechnique.Skills.SelectMany(s => s.Launches).SelectMany(s => s.Projectile.Damage).Sum(); }
I would also need to make sure that in my
AddProjectile()
,AddSkill()
, andAddProjectileSet()
methods, I add the lineRaisePropertyChanged(() => TotalDamage);
, but that isn't too bad.