11
\$\begingroup\$

I made a Tic-Tac-Toe game in WPF and was just hoping for some input as I am pretty new to WPF and I am still a student so I'm sure that my C# code in general may be lacking

XAML:

<Window x:Class="TicTacToc.MainWindow"
    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:local="clr-namespace:TicTacToc"
    mc:Ignorable="d"
    Title="MainWindow" Height="400" Width="400" ResizeMode="NoResize">
<Grid>
    <DockPanel>
        <StackPanel DockPanel.Dock="Bottom"
                    Background="#FF8C80B4">
            <Button Click="Restart_Click"
                    Content="Restart"
                    FontSize="20"
                    Height="50"
                    Width="100"
                    VerticalAlignment="Center"
                    HorizontalAlignment="Center"
                    Background="#FFF0F0F0" />
        </StackPanel>
        <Border BorderBrush="White"
                BorderThickness="5">
            <Grid x:Name="MyGrid">
                <Grid.RowDefinitions>
                    <RowDefinition Height="33*" />
                    <RowDefinition Height="33*" />
                    <RowDefinition Height="33*" />
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="33*" />
                    <ColumnDefinition Width="33*" />
                    <ColumnDefinition Width="33*" />
                </Grid.ColumnDefinitions>
                <Grid.Resources>
                    <Style TargetType="Button">
                        <Setter Property="BorderThickness" Value="2"/>
                        <Setter Property="BorderBrush"
                                Value="#FF423277" />
                        <Setter Property="FontSize" Value="30"/>
                    </Style>

                    <BooleanToVisibilityConverter x:Key="BoolToVisibility" />

                </Grid.Resources>
                <Button x:Name="TopXLeft"
                        Click="Button_Click"
                        Grid.Row="0"
                        Grid.Column="0"/>
                <Button Grid.Row="0"
                        Grid.Column="1"
                        x:Name="TopXMiddle"
                        Click="Button_Click"/>
                <Button Grid.Row="0"
                        Grid.Column="2"
                        x:Name="TopXRight"
                        Click="Button_Click"/>
                <Button x:Name="CenterXLeft"
                        Grid.Row="1"
                        Grid.Column="0"
                        Click="Button_Click"/>
                <Button x:Name="CenterXMiddle"
                        Grid.Row="1"
                        Grid.Column="1"
                        Click="Button_Click"/>
                <Button x:Name="CenterXRight"
                        Grid.Row="1"
                        Grid.Column="2"
                        Click="Button_Click"/>
                <Button x:Name="BottomXLeft"
                        Grid.Row="2"
                        Grid.Column="0"
                        Click="Button_Click" />
                <Button x:Name="BottomXMiddle"
                        Grid.Row="2"
                        Grid.Column="1"
                        Click="Button_Click" />
                <Button x:Name="BottomXRight"
                        Grid.Row="2"
                        Grid.Column="2"
                        Click="Button_Click"/>

                <Label Grid.Row="1"
                       Grid.ColumnSpan="3"
                       Background="#FF8C80B4"
                       Foreground="WhiteSmoke"
                       Content="Game Won"
                       FontSize="30"
                       VerticalContentAlignment="Center"
                       HorizontalContentAlignment="Center"
                       Visibility="{Binding Path=HasWon, Converter={StaticResource BoolToVisibility}}">
                </Label>

            </Grid>
        </Border>
    </DockPanel>
</Grid>

C# File:

namespace TicTacToc
{
    public partial class MainWindow : Window
    {
        public GameBoard MyGameBoard = new GameBoard();

        public MainWindow()
        {
            InitializeComponent();
            this.DataContext = MyGameBoard;
        }

        public void Button_Click(object sender, RoutedEventArgs e)
        {//Updates UI and calls gameBoard update
            var clickedButton = sender as Button;

            if(MyGameBoard.currentPlayer == GameBoard.CurrentPlayer.X)
            {
                clickedButton.Foreground = (SolidColorBrush)(new BrushConverter().ConvertFrom("#811717"));
            }
            else if (MyGameBoard.currentPlayer == GameBoard.CurrentPlayer.O)
            {
                clickedButton.Foreground = (SolidColorBrush)(new BrushConverter().ConvertFrom("#126712"));
            }
            clickedButton.Background = Brushes.WhiteSmoke;
            clickedButton.Content = MyGameBoard.currentPlayer;
            clickedButton.IsHitTestVisible = false;

            MyGameBoard.UpdateBoard(clickedButton.Name);
        }

        private void Restart_Click(object sender, RoutedEventArgs e)
        {//Restarts Game
            for(int i = 0; i < VisualTreeHelper.GetChildrenCount(MyGrid) - 1; i++) // This loop iterates through all the buttons/tiles in the grid and sets changed properties to default
            {
                var child = VisualTreeHelper.GetChild(MyGrid, i) as Button;
                child.Content = null;
                child.IsHitTestVisible = true;
                child.Background = (SolidColorBrush)(new BrushConverter().ConvertFrom("#FFDDDDDD"));
            }
            MyGameBoard = new GameBoard();
            this.DataContext = MyGameBoard;
        }
    }

    public class GameBoard : INotifyPropertyChanged
    {
        //Variables to be used
        public enum CurrentPlayer
        {
            X = 1,
            O
        }

        private int turn = 1;
        public CurrentPlayer currentPlayer = CurrentPlayer.X;
        private bool hasWon = false;
        public bool HasWon
        {
            get { return hasWon; }
            set { hasWon = value; NotifyPropertyChanged("HasWon"); }
        }

        private Dictionary<string, int> board = new Dictionary<string, int>()
            {
                {"TopXLeft",0 },
                {"TopXMiddle",0 },
                {"TopXRight",0 },
                {"CenterXLeft",0 },
                {"CenterXMiddle",0 },
                {"CenterXRight",0 },
                {"BottomXLeft",0 },
                {"BottomXMiddle",0 },
                {"BottomXRight",0 }
            };

        public void NotifyPropertyChanged(string info)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(info));
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;


        private bool CheckIfWon(string buttonName)
        {//Calls all methods that check if a game has been won
            if (WonInRow(buttonName))
            {
                return true;
            }
            else if (WonInColumn(buttonName))
            {
                return true;
            }
            else if (WonInDiagonal(buttonName))
            {
                return true;
            }
            else
                return false;
        }

        private bool WonInRow(string name)
        {//Checks to see if a player has just won through having three pieces in the tile's row
            string row = name.Substring(0,name.IndexOf('X') - 1);

            foreach(var element in board)
            {
                string keyName = element.Key;
                string rowOfElement = keyName.Substring(0,keyName.IndexOf('X') - 1);

                if(rowOfElement == row)
                {
                    if (element.Value != (int)currentPlayer)
                        return false;
                }
            }

            return true;
        }

        private bool WonInColumn(string name)
        {//Checks to see if player has jsut won thorugh having three pieces in the tile's column
            string column = name.Substring(name.IndexOf('X') + 1);

            foreach (var element in board)
            {
                string keyName = element.Key;
                string columnOfElement = keyName.Substring(keyName.IndexOf('X') + 1);

                if (columnOfElement == column)
                {
                    if (element.Value != (int)currentPlayer)
                        return false;
                }
            }

            return true;
        }

        private bool WonInDiagonal(string name)
        {//Checks to see if player has just won by having three pieces diagonally
            if (name == "TopXLeft" || name == "CenterXMiddle" || name == "BottomXRight")
            {
                if (board["CenterXMiddle"] == (int)currentPlayer && board["BottomXRight"] == (int)currentPlayer && board["TopXLeft"] == (int)currentPlayer)
                {
                    return true;
                }
                else
                    return false;
            }
            if (name == "TopXRight" || name == "CenterXMiddle" || name == "BottomXLeft")
            {
                if (board["CenterXMiddle"] == (int)currentPlayer && board["BottomXLeft"] == (int)currentPlayer && board["TopXRight"] == (int)currentPlayer)
                {
                    return true;
                }
                else
                    return false;
            }
            else
                return false;
        }

        private void UpdateDictionary(string buttonName)
        {//Update the dictionary by changing the value of the selected tile(key) to the players value
            string tileName = buttonName;

            board[tileName] = (int)currentPlayer;
        }

        public void UpdateBoard(string buttonName)
        {//Handles logic of game by updating board and checking win conditions, called on tile click
            UpdateDictionary(buttonName);

            if (turn >= 5)//Earliest turn a player can win
            {
                if (CheckIfWon(buttonName))
                {
                    HasWon = true;
                }
            }

            turn++;

            if (currentPlayer == CurrentPlayer.X)
                currentPlayer = CurrentPlayer.O;

            else if (currentPlayer == CurrentPlayer.O)
                currentPlayer = CurrentPlayer.X;
        }
    }
}

I would appreciate any type of input for WPF or just my code in general. I do feel that I may have problems specifically in my use of WPF and data binding/UI-Logic seperation as well as my naming conventions.

\$\endgroup\$
  • \$\begingroup\$ How do I format my code blocks? will help you. \$\endgroup\$ – Tushar Mar 13 '17 at 5:28
  • \$\begingroup\$ Sorry, just couldn't get that one part.But Thank you and Ill make sure to look it over properly \$\endgroup\$ – TrillKouda Mar 13 '17 at 6:30
  • \$\begingroup\$ Welcome to Code Review! I hope you get great answers. \$\endgroup\$ – Phrancis Mar 13 '17 at 6:37
3
\$\begingroup\$

First, your grids aren't based on chunks of 100:

<Grid.RowDefinitions>
    <RowDefinition Height="33*" />
    <RowDefinition Height="33*" />
    <RowDefinition Height="33*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
    <ColumnDefinition Width="33*" />
    <ColumnDefinition Width="33*" />
    <ColumnDefinition Width="33*" />
</Grid.ColumnDefinitions>

Rather than using "33*", those should just be "1*". If you wanted the middle one twice as wide as the rows/columns on either side, then you would give that one "2*", and so on.


Now for an MVVM lesson. You almost never want to have code in your code-behind for the view. What you should do is set up a ViewModel.

So you have your view MainWindow.xaml:

<!-- your XAML goes here -->

Then your code-behind MainWindow.xaml.cs:

/* don't touch this file */

And your VM MainWindowViewModel.cs:

public class MainWindowViewModel : INotifyPropertyChanged
{
    // store all your data in properties here; fire the `PropertyChanged` event in the setter

    // example property
    private int _foo;
    public int Foo
    {
        get { return _foo; }
        set
        {
            _foo = value;
            OnPropertyChanged();
        }
    }

    // implement a `RelayCommand` or `DelegateCommand` class off the `ICommand` interface; then use instances of it to create commands
    // these commands will bind to the `Command` element of `Button`s, etc.

    // example command: assign this in the ctor of your VM
    public DelegateCommand FooCommand { get; }
}

See these files for the necessary files to set up the Command structure: https://github.com/Hosch250/CheckersUI/tree/master/CheckersUI/Commands

Now, the easy part is wiring this up to the UI; the tricky part might be understanding the wiring.

So first, we need to let the UI know where to look for the data. We set the DataContext:

var mainWindow = new MainWindow();
var mainWindowVM = new MainWindowViewModel();
mainWindow.DataContext = mainWindowVM;

And now we can use our VM in the view:

<Button Text="Foo" Command="{Binding FooCommand}" />

Now, clicking this button calls the FooCommand command in your VM. (If you have one additional piece of data to pass to the command, you can use the CommandParameter binding.)

And displaying a value:

<TextBox Text="{Binding Foo}" />

You can do bindings on colors, and more. Converters are also useful:

Here is an example implementation of a converter:

public class FooConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture) =>
        (int) value == 0 ? Colors.Blue : Colors.Black;

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) =>
        throw new NotImplementedException(); // this converter is just for the display--this method is never called
}

I hope this makes sense; I know it can appear somewhat convoluted, but it really will make your code cleaner and easier to maintain. Basically, all your logic and display data goes in the VM, and the view is for pure presentation. Because the code-behind is part of the view, it should also only be involved in presentation details that cannot be expressed in the XAML, like fancy drag/drop details. Feel free to ping me if you have any questions.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.