Magus' Exhibitionist Code



  • Continuing the discussion from .NET (WPF) CollectionViews and Filtering:

    @GOG said:

    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.


  • Trolleybus Mechanic

    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.


  • Trolleybus Mechanic

    @Magus said:

    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 different ObservableCollection<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.


  • Trolleybus Mechanic

    @Magus said:

    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(), and AddProjectileSet() methods, I add the line RaisePropertyChanged(() => TotalDamage);, but that isn't too bad.


Log in to reply