开发者

How do I bind a simple array?

开发者 https://www.devze.com 2023-03-22 21:07 出处:网络
I\'m making a scrabble-like game in WPF C#. As of current I\'ve got the AI Algorithm working with operates on a \"Model Game Board\", a variable string[15,15]. However over the past few days I\'m stuc

I'm making a scrabble-like game in WPF C#. As of current I've got the AI Algorithm working with operates on a "Model Game Board", a variable string[15,15]. However over the past few days I'm stuck at producing a GUI to display this Array as a GameBoard.

As of current I've got the following XAML Code:

My "MainWindow" which contains:

A button:

<Button Click="Next_TurnClicked" Name="btnNext_Turn">Next Turn</Button>

A UserControl: Which is the Game Board(GameBoard is also another UserControl) and the Player's Rack

<clr:Main_Control></clr:Main_Control>

Then Inside my UserControl I have:

    <DockPanel Style ="{StaticResource GradientPanel}">
    <Border Style ="{StaticResource ControlBorder}" DockPanel.Dock="Bottom">
        <ItemsControl VerticalAli开发者_Python百科gnment="Center" HorizontalAlignment="Center">
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <StackPanel Height ="Auto" Name="ListBox" Orientation="Horizontal" HorizontalAlignment="Center">
                    </StackPanel>
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
            <clr:Rack_Cell_Sender x:Name="Player_Tile_1" ></clr:Rack_Cell_Sender>
            <clr:Rack_Cell_Sender x:Name="Player_Tile_2" ></clr:Rack_Cell_Sender>
            <clr:Rack_Cell_Sender x:Name="Player_Tile_3" ></clr:Rack_Cell_Sender>
            <clr:Rack_Cell_Sender x:Name="Player_Tile_4" ></clr:Rack_Cell_Sender>
            <clr:Rack_Cell_Sender x:Name="Player_Tile_5" ></clr:Rack_Cell_Sender>
        </ItemsControl>
    </Border>
    <Viewbox Stretch="Uniform">
        <Border Margin="5" Padding="10" Background="#77FFFFFF" BorderBrush="DimGray" BorderThickness="3">
            <Border BorderThickness="0.5" BorderBrush="Black">
                <clr:GameBoard>
                </clr:GameBoard>
            </Border>
        </Border>
    </Viewbox>
</DockPanel>

clr:GameBoard is a ItemsControl with its ItemsPanelTemplate as a UniformGrid

        <Style TargetType="ItemsControl">
        <Setter Property="ItemsPanel">
            <Setter.Value>
                <ItemsPanelTemplate>
                    <UniformGrid IsItemsHost="True" Margin="0" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Rows="15" Columns="15" />
                </ItemsPanelTemplate>
            </Setter.Value>
        </Setter>
        <Setter Property="ItemTemplate">
            <Setter.Value>
                <DataTemplate>
                    <Words:Cell>
                    </Words:Cell>
                </DataTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</UserControl.Resources>
<ItemsControl Name="BoardControl" ItemsSource="{DynamicResource CellCollectionData}">
</ItemsControl>

How do I bind a simple array?

So my question is:

  1. How do I get my array[,] to the ItemsControl? I have no ideas how to DataBind, I've read a couple of tutorials but I'm only starting to get what a DataContext is.
  2. How will I refresh the Board after each turn? I don't want the board updating before btnNext_Turn is clicked.
  3. How do I Update the ModelBoard after user inputs new word into UI and btn_Next_Turn is clicked?

I'm a beginner in coding and this is my first real project in C# and WPF. My teacher knows neither WPF or C# so you guys on the StackoverFlow community has been a great help over the past few weeks, especially helping me out on SQL.

Please, any help will be much appreciated!

UPDATE:

Thanks Erno for the quick response! Yea, I got an error for EventHandler so I swapped it out for PropertyChangedEventHandler and that stopped the errors.

public partial class GameBoard : UserControl
{
    TileCollection RefreshTiles = new TileCollection();
    public GameBoard()
    {
        InitializeComponent();
    }
    public void getBoard()
    {
        string[,] ArrayToAdd = InnerModel.ModelBoard;
        for (int i = 0; i < 15; i++)
        {
            for (int j = 0; j < 15; j++)
            {
                Tile AddTile = new Tile();
                AddTile.Charater = ArrayToAdd[i, j];
                AddTile.X = i;
                AddTile.Y = j;
                RefreshTiles.Add(AddTile);
            }
        }
    }
}

So when I run debug I can see the RefreshTiles Collection being filled. However how do I bind the Collection to the ItemsControl?

Do I set the DataContext of the UserControl to RefreshTiles?

this.DataContext = RefreshTiles

then in XAML

<ItemsControl Name ="BoardControl" ItemsSource="{Binding}">

Update:

So apparently if I set the bind up in the MainWindow it works perfectly however this does not work when i try binding from a Usercontrol? I set a breakpoint in "RefreshArray" and I can see it being populated, however the UserControl does not update?

public partial class UserControl1 : UserControl
{
    public char GetIteration
    {
        get { return MainWindow.Iteration; }
        set { MainWindow.Iteration = value; }
    }
    CellCollection NewCells = new CellCollection();
    public UserControl1()
    {
        InitializeComponent();
        this.DataContext = NewCells;
        PopulateCells();
    }
    private void PopulateCells()
    {
        for (int i = 0; i < 15; i++)
        {
            for (int j = 0; j < 15; j++)
            {
                Cell NewCell = new Cell();
                NewCell.Character = "A";
                NewCell.Pos_x = i;
                NewCell.Pos_y = j;
                NewCells.Add(NewCell);
            }
        }
    }
    public void RefreshArray()
    {
        NewCells.Clear();
        for (int i = 0; i < 15; i++)
        {
            for (int j = 0; j < 15; j++)
            {
                Cell ReCell = new Cell();
                ReCell.Character = GetIteration.ToString();
                ReCell.Pos_x = i;
                ReCell.Pos_y = j;
                NewCells.Add(ReCell);
            }
        }
        this.DataContext = NewCells;
    }
}

public partial class MainWindow : Window
{
    UserControl1 Control = new UserControl1();
    public static char Iteration = new char();
    public MainWindow()
    {
        InitializeComponent();
    }


    private void Next_Click(object sender, RoutedEventArgs e)
    {
        Iteration = 'B';
        Control.RefreshArray();
    }
}

This doesn't work while the one below does work

public partial class MainWindow : Window
{
    char Iteration = new char();
    CellCollection NewCells = new CellCollection();
    public MainWindow()
    {
        InitializeComponent();
        PopulateCells();
        this.DataContext = NewCells;
        Iteration++;
    }
    private void PopulateCells()
    {
        for (int i = 0; i < 15; i++)
        {
            for (int j = 0; j < 15; j++)
            {
                Cell NewCell = new Cell();
                NewCell.Character = "A";
                NewCell.Pos_x = i;
                NewCell.Pos_y = j;
                NewCells.Add(NewCell);
            }
        }
    }
    private void RefreshArray()
    {
        NewCells.Clear();
        for (int i = 0; i < 15; i++)
        {
            for (int j = 0; j < 15; j++)
            {
                Cell ReCell = new Cell();
                ReCell.Character = Iteration;
                ReCell.Pos_x = i;
                ReCell.Pos_y = j;
                NewCells.Add(ReCell);
            }
        }
    }

    private void Next_Click(object sender, RoutedEventArgs e)
    {
        RefreshArray();
    }
}


There is no short answer to this question, so I'll give an outline feel free to ask more questions to get more details where needed:

To use databinding make sure the items in the collection implement INotifyPropertyChanged. Currently you are using an array of strings. String do not implement INotifyPropertyChanged.

Also make sure the collection implements INotifyCollectionChanged. Currently you are using an two dimensional array. An array does not implement this interface.

A solution would be to create a class named Tile that implements INotifyPropertyChanged and stores the character as a string and additionally stores its position on the board in an X and Y property:

public class Tile : INotifyPropertyChanged
{
    private string character;
    public string Character
    {
        get
        {
            return character;
        }
        set
        {
            if(character != value)
            {
                character = value;
                OnPropertyChanged("Character");
            }
        }
    }

    private int x; // repeat for y and Y
    public int X
    {
        get
        {
            return x;
        }
        set
        {
            if(x != value)
            {
                x = value;
                OnPropertyChanged("X");
            }
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged(string propertyName)
    {
        var p = PropertyChanged;
        if(p != null)
        {
            p(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

Create a class Tiles like this:

public class Tiles : ObservableCollection<Tile>
{
}

and replace the two dimensional array with this collection.

One way of binding an items control to the collection requires you to set the DataContext of the items control to an instance of the collection and specifying the property ItemsSource="{Binding}"

Use the Character, X and Y properties in the itemtemplate to display the text and position the tile.

This way the bindings will automagically update the view when you manipulate the Tiles collection when adding or removing tiles and the board will also be updated when a tile changes (either position or content)


Emo's approach is a good one; I'd suggest a slightly different tack.

First, set your existing program aside. You'll come back to it later.

Next, implement a prototype WPF project. In this project, create a class that exposes Letter, Row, and Column properties, and another class that exposes a collection of these objects. Write a method that fills this collection with test data.

In your main window, implement an ItemsControl to present this collection. This control needs four things:

  1. Its ItemsPanel must contain a template for the panel it's going to use to arrange the items it contains. In this case, you'll be using a Grid with rows and columns of a predefined size.

  2. Its ItemContainerStyle must contain setters that tell the ContentPresenter objects that the template generates which row and column of the grid they belong in.

  3. Its ItemTemplate must contain a template that tells it what controls it should put in the ContentPresenters.

  4. Its ItemsSource must be bound to the collection of objects.

A minimal version looks like this:

<ItemsControl ItemsSource="{Binding}">
  <ItemsControl.ItemsPanel>
    <ItemsPanelTemplate>
      <Grid>
        <Grid.RowDefinitions>
          <RowDefinition Height="10"/>
          <RowDefinition Height="10"/>
          <RowDefinition Height="10"/>
          <RowDefinition Height="10"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
          <ColumnDefinition Width="10"/>
          <ColumnDefinition Width="10"/>
          <ColumnDefinition Width="10"/>
          <ColumnDefinition Width="10"/>
        </Grid.ColumnDefinitions>
      </Grid>      
    </ItemsPanelTemplate>
  </ItemsControl.ItemsPanel>
  <ItemsControl.ItemContainerStyle>
    <Style TargetType="ContentPresenter">
      <Setter Property="Grid.Row" Value="{Binding Row}"/>
      <Setter Property="Grid.Column" Value="{Binding Column}"/>
    </Style>
  </ItemsControl.ItemContainerStyle>
  <ItemsControl.ItemTemplate>
    <DataTemplate TargetType="{x:Type MyClass}">
      <TextBlock Text="{Binding Letter}"/>
    </DataTemplate>
  </ItemsControl.ItemTemplate>
</ItemsControl>

Set the DataContext of the main window to a populated instance of your collection, and you should see it lay the letters out in a 4x4 grid.

How this works: By setting ItemsSource to {Binding}, you're telling the ItemsControl to get its items from the DataContext. (Controls inherit their DataContext from their parent, so setting it on the window makes it available to the ItemsControl).

When WPF renders an ItemsControl, it creates a panel using the ItemsPanelTemplate and populates it with items. To do this, it goes through the items in the ItemsSource and, for each, generates a ContentPresenter control.

It populates the Content property of that control using the template found in the ItemTemplate property.

It sets properties on the ContentPresenter using the style found in the ItemContainerStyle property. In this instance, the style sets the Grid.Row and Grid.Column attached properties, which tell the Grid where to put them when it draws them on the screen.

So the actual objects that get created look like this:

<Grid>
  <Grid.RowDefinitions>
    <RowDefinition Height="10"/>
    <RowDefinition Height="10"/>
    <RowDefinition Height="10"/>
    <RowDefinition Height="10"/>
  </Grid.RowDefinitions>
  <Grid.ColumnDefinitions>
    <ColumnDefinition Width="10"/>
    <ColumnDefinition Width="10"/>
    <ColumnDefinition Width="10"/>
    <ColumnDefinition Width="10"/>
  </Grid.ColumnDefinitions>
  <ContentPresenter Grid.Row="0" Grid.Column="0">
    <ContentPresenter.Content>
      <TextBlock Text="A"/>
    </ContentPresenter.Content>
  </ContentPresenter>
  <ContentPresenter Grid.Row="1" Grid.Column="1">
    <ContentPresenter.Content>
      <TextBlock Text="A"/>
    </ContentPresenter.Content>
  </ContentPresenter>
</Grid>      

(No actual XAML gets created, but the above XAML is a pretty good representation of the objects that do.)

Once you have this working, you now have a bunch of relatively straightforward problems to solve:

  1. How do you make this look more like what you want it to look like? The answer to this is going to involve making a more elaborate ItemTemplate.

  2. How do you sync the collection that this is presenting up with the array in your application? This depends (a lot) on how your application is designed; one approach is wrapping the collection in a class, having the class create an instance of your back-end object model, and having the back-end object model raise an event every time its collection changes, so that the class containing the collection of objects that are being presented in the UI knows to create a new front-end object and add it to its collection.

  3. How does the user select a cell in the grid to put a tile into? The answer to this is probably going to involve creating objects for all the cells, not just the ones that contain letters, implementing a command to gets executed when the user clicks on the cell, and changing the ItemTemplate so that it can executes this command when the user clicks on it.

  4. If the contents of a cell change, how does the UI find out about it? This is going to require implementing INotifyPropertyChanged in your UI object class, and raising PropertyChanged when Letter changes.

The most important thing about the approach I'm recommending here, if you haven't noticed, is that you can actually get the UI working almost completely independently of what you're doing in the back end. The UI and the back end are coupled together only in that first class you created, the one with the Row, Column, and Letter properties.

This, by the way, is a pretty good example of the Model/View/ViewModel pattern that you've probably heard about if you're interested in WPF. The code you've written is the model. The window is the view. And that class with the Row, Column, and Letter properties is the view model.

0

精彩评论

暂无评论...
验证码 换一张
取 消