How to fill a ComboBox with values from a list, an

2019-09-04 08:14发布

TL;DR at bottom

A little back story for context. I'm creating an xml tool that will enable folks to input data needed for a game we're making without having to worry about hand-jobbing XML directly. The tool will output various XML files to be used by the game engine (for example, complex NPC Dialogue nodes with references to scripts to run when a given piece of dialogue is used... basically all the data to feed the game engine).

I'm stuck trying to figure out a perplexing issue I'm having...

I'm creating a list view (as a user control named ActionsBox - need re-usability on it), with a data template that contains a ComboBox. The ComboBox needs to have its values filled from an xml config file (readonly, but I made it a full viewmodel for constancy), but the selected item will need to be bound to a string property in a separate viewmodel (which store actual content instead of config/static things). From what I've read this won't really work as controls can't be bound to two different things.

The purpose of this bit is to constrain people using the tool to only select appropriate values (i.e. make sure they only select scripts that actually exist in the game engine. We'll be updating this config XML file every time we add a new script to the engine that can be called from XML).

I have a feeling I might be going about this wrong (over?)architecturally, but can't really think of any good way to solve it (and google isn't being all that helpful).

What's the best way to tackle this approach (code and architecture detail follows)?

Xml (The goal is to fill this combobox with script names):

<ConditionScripts>
    <Script>
      <Name>Test Script 1</Name>
      <Description>This script does x</Description>
      <Parameters>
        <Parameter>
          <Name>TestStr</Name>
          <Type>string</Type>
          <Description>Blah blah</Description>
        </Parameter>
      </Parameters>
    </Script>
    <Script>
      <Name>Test Script 2</Name>
      <Description>This script does y</Description>
      <Parameters>
        <Parameter>
          <Name>TestStr</Name>
          <Type>string</Type>
          <Description>Blah blah</Description>
        </Parameter>
        <Parameter>
          <Name>TestInt</Name>
          <Type>int</Type>
          <Description>BlahBlah</Description>
        </Parameter>
      </Parameters>
    </Script>
  </ConditionScripts>

Config viewmodel holding the name:

public class ConfigScriptViewModel : ViewModelBase
{
    #region Properties
    private string name;
    public string Name
    {
        get { return Name; }
        set 
        { 
            name = value;
            OnPropertyChanged();
        }
    }

    private string description;
    public string Description
    {
        get { return description; }
        set
        {
            description = value;
            OnPropertyChanged();
        }
    }

    private ObservableCollection<ConfigScriptParameterViewModel> parameters;
    public ObservableCollection<ConfigScriptParameterViewModel> Parameters
    {
        get { return parameters; }
        set
        {
            parameters = value;
            OnPropertyChanged();
        }
    }
    #endregion

    public ConfigScriptViewModel(XElement scriptNode)
    {
        this.Name = scriptNode.Element("Name").Value;
        this.Description = scriptNode.Element("Description").Value;

        var paramsVm = new ObservableCollection<ConfigScriptParameterViewModel>();

        foreach (var param in scriptNode.Element("Parameters").Elements())
        {
            var paramVm = new ConfigScriptParameterViewModel(param);
            paramsVm.Add(paramVm);
        }

        this.Parameters = paramsVm;
    }
}

The above is held in another ViewModel - ConfigViewModel which has them stored in an ObservableCollection and is the highest level of the "Config" branch. Code available upon request, but left out here to save space for now (since it's just a container VM).

There's one ViewModel level higher, the MasterViewModel which contains the aforementioned ConfigViewModel as well as the other high level ViewModels (such as the DialoguesViewModel, which is one place of several the ActionsBox will be used).

Changing gears to the branch of ViewModels that hold actual data -

The ScriptViewModel contains the ScriptName property that needs to be set based off the selected item of the combobox (and have the combobox set to that item on initial load). Ultimately this will wind up in an (different) xml file (the various XML files are my model).

ScriptViewModel:

public class ScriptViewModel : ViewModelBase
{
    private ObservableCollection<ScriptArgumentViewModel> arguments;

    public ObservableCollection<ScriptArgumentViewModel> Arguments
    {
        get { return arguments; }
        set
        {
            arguments = value;
            OnPropertyChanged();
        }
    }

    private string scriptName;

    public string ScriptName
    {
        get { return scriptName; }
        set
        {
            scriptName = value;
            OnPropertyChanged();
        }
    }

    public ScriptViewModel(XElement scriptNode)
    {
        originalModel = scriptNode;
        ScriptName = (string)scriptNode.Attribute("ScriptName");


        var args = new ObservableCollection<ScriptArgumentViewModel>();

        foreach (var arg in scriptNode.Elements(CommonTypesNS + "Argument"))
        {
            var a = new ScriptArgumentViewModel(arg);
            args.Add(a);
        }

        if (args.Count > 0)
        { Arguments = args; }
    }
}

The ScriptVM(s) are held in an ActionsViewModel in an ObservableCollection, as an Action can have 0 or many scripts. Again, that code is omitted for shortness sake, but is available if needed. Actions are reused throughout several other viewmodels (they're a high re-usable bit).

Finally we get to the user control I'm making.

UserControl w/ ListView XAML:

<UserControl x:Class="SavatronixXmlTool.Code.UserControls.ActionsBox"
         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:stxVm="clr-namespace:SavatronixXmlTool.Code.ViewModels"
         mc:Ignorable="d" 
         d:DesignHeight="300" d:DesignWidth="300">
<ListView x:Name="ActionsListView">
    <ListView.Resources>
        <!--Script data template-->
        <DataTemplate DataType="{x:Type stxVm:ScriptVM}">
            <StackPanel Orientation="Horizontal">
                <TextBlock>Type: Script</TextBlock>
                <Label>Script Name:</Label>
                <!-- stuck here >.< -->
                <ComboBox />
            </StackPanel>
        </DataTemplate>
    </ListView.Resources>
</ListView>

I figured since the ActionsBox can be used in different places, with potentially different inherited data contexts, I needed to normalize. So I used Dependency Properties in the code-behind to take an ActionsViewModel (and set DataContext = this; in the ActionsBox constructor).

For contexts sake (and for those with piqued interest) the target output XML of the tool will look something like...

                  <DirectDialogue Id="Rachel-DD-12" >
                    <Text>And talking some more!</Text>
                    <Conditions></Conditions>
                    <Actions>
                      <cmn:Script ScriptName="TestScript">
                        <cmn:Argument Name="testInt" Type="int" Value="5"/>
                      </cmn:Script>
                    </Actions>
                    <Voice></Voice>
                    <Animation></Animation>
                    <Notes></Notes>
                  </DirectDialogue>

With the script bit(and action, across differing xml files, like Quests) node being a highly reusable piece to the overall puzzel.

TL;DR**** Anyway, I'd appreciate any help with figuring out how to load values in a combobox from one source, yet have changes to SelectedItem bound to a different place. Many thanks will be eternally given.

1条回答
太酷不给撩
2楼-- · 2019-09-04 08:53

What this all boils down to is that your ScriptVM needs access to the list of possible script names, which are held deep in your collection of ConfigVMs. It can then expose these as another property that your combo can bind to.

How you do this depends on the structure of your app. Dependency injection is generally favored which would mean that your ScriptVM gets the list passed into its constructor.

Passing the ObservableCollection of ConfigVMs feels like a violation of separation of concerns. It looks like ConfigVM holds a lot of other stuff that ScriptVM shouldn't care about. Neither should it have to know how to navigate the ConfigVM->ConfigScriptVM->Name tree.

I would favour just passing in an ObservableCollection of name strings that is maintained elsewhere in the app, or possibly some IScriptNameProvider interface, again implemented where it makes sense (whatever manages your ConfigVM collection)

The only other ways are nasty, such as having a static config class that the other VMs can access.

查看更多
登录 后发表回答