AEvers
AEvers

Reputation: 85

Programmatically adjusting the number of rows in a grid with C#

I'm currently teaching myself XAML/C# and writing a calendar application. Right now it's creating a grid and then applying user control elements to the grid. It is correctly building my calendar but, instead of defining the number of rows in XAML, I want to be able to set the number via C# dynamically. Some months use more or less weeks (March needs 5 but April needs 6). I'm wondering how to do that or if I should be using a different control than grid.

This is what the UI looks like.

XAML code

<UserControl x:Class="CMS.Control.MonthView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    HorizontalAlignment="Stretch" VerticalAlignment="Stretch" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch">

    <Grid VerticalAlignment="Stretch">
        <Grid.RowDefinitions>
            <RowDefinition Height="40"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <StackPanel Orientation="Horizontal" Background="AliceBlue">
            <Image x:Name="MonthPrev" Source="/Images/Previous.png" Height="24" Margin="16,0,6,0" MouseLeftButtonUp="MonthPrev_MouseLeftButtonUp"/>
            <Image x:Name="MonthNext" Source="/Images/Next.png" Height="24" Margin="6,0,16,0" MouseLeftButtonUp="MonthNext_MouseLeftButtonUp"/>
            <Label x:Name="DateLabel" Content="January 2017" FontSize="16" FontFamily="Bold" VerticalAlignment="Center"/>
        </StackPanel>
        <Grid Grid.Row="1" Background="AliceBlue">
            <Grid.ColumnDefinitions>

                <ColumnDefinition MinWidth="60" Width="*"/>
                <ColumnDefinition MinWidth="60" Width="*"/>
                <ColumnDefinition MinWidth="60" Width="*"/>
                <ColumnDefinition MinWidth="60" Width="*"/>
                <ColumnDefinition MinWidth="60" Width="*"/>
                <ColumnDefinition MinWidth="60" Width="*"/>
                <ColumnDefinition MinWidth="60" Width="*"/>
            </Grid.ColumnDefinitions>
            <Label Grid.Column="0" Content="Sunday" FontSize="9" Margin="2,0,0,2" Padding="0,1,0,0" HorizontalAlignment="Center" VerticalAlignment="Center" BorderThickness="0,0,1,0"/>
            <Label Grid.Column="1" Content="Monday" FontSize="9" Margin="2,0,0,2" Padding="0,1,0,0" HorizontalAlignment="Center" VerticalAlignment="Center" BorderThickness="0,0,1,0"/>
            <Label Grid.Column="2" Content="Tuesday" FontSize="9" Margin="2,0,0,2" Padding="0,1,0,0" HorizontalAlignment="Center" VerticalAlignment="Center" BorderThickness="0,0,1,0"/>
            <Label Grid.Column="3" Content="Wednesday" FontSize="9" Margin="2,0,0,2" Padding="0,1,0,0" HorizontalAlignment="Center" VerticalAlignment="Center" BorderThickness="0,0,1,0"/>
            <Label Grid.Column="4" Content="Thursday" FontSize="9" Margin="2,0,0,2" Padding="0,1,0,0" HorizontalAlignment="Center" VerticalAlignment="Center" BorderThickness="0,0,1,0"/>
            <Label Grid.Column="5" Content="Friday" FontSize="9" Margin="2,0,0,2" Padding="0,1,0,0" HorizontalAlignment="Center" VerticalAlignment="Center" BorderThickness="0,0,1,0"/>
            <Label Grid.Column="6" Content="Saturday" FontSize="9" Margin="2,0,0,2" Padding="0,1,0,0" HorizontalAlignment="Center" VerticalAlignment="Center" BorderThickness="0,0,1,0"/>
        </Grid>

        <Grid x:Name="WeekRowGrid" Grid.Row="2">
            <Grid.ColumnDefinitions>

                    <ColumnDefinition Width="*"/>
                    <ColumnDefinition Width="*"/>
                    <ColumnDefinition Width="*"/>
                    <ColumnDefinition Width="*"/>
                    <ColumnDefinition Width="*"/>
                    <ColumnDefinition Width="*"/>
                    <ColumnDefinition Width="*"/>
                </Grid.ColumnDefinitions>

            <Grid.RowDefinitions>
                <RowDefinition Height="*"/>
                <RowDefinition Height="*"/>
                <RowDefinition Height="*"/>
                <RowDefinition Height="*"/>
                <RowDefinition Height="*"/>
                <RowDefinition Height="*"/>
            </Grid.RowDefinitions>
        </Grid>
    </Grid>
</UserControl>

C# Code

namespace CMS.Control
{
    /// <summary>
    /// Interaction logic for MonthView.xaml
    /// </summary>
    public partial class MonthView : UserControl
    {
        private DateTime _DispayDate;

        public MonthView()
        {
            InitializeComponent();
            _DispayDate = DateTime.Now;
            DrawMonth();
        }

        //Generates the 
        private void DrawMonth()
        {
            DateTime FirstDayOfMonth = new DateTime(_DispayDate.Year, _DispayDate.Month, 1);
            int DisplayFrontPadding = (int)FirstDayOfMonth.DayOfWeek; // # of days that need to be displayed before the 1st of the month
            int DaysInDisplayMonth = DateTime.DaysInMonth(_DispayDate.Year, _DispayDate.Month);
            int DaysInDisplay = DisplayFrontPadding + DaysInDisplayMonth; 
            DaysInDisplay += 7 - DaysInDisplay%7; // Rounds up the displayed days to a multiple of 7

            DateLabel.Content = _DispayDate.ToString("MMMM") + " " + _DispayDate.Year;

            for (int i = 0; i<DaysInDisplay; i++)
            {
                DateTime DisplayDay = FirstDayOfMonth.AddDays(i - DisplayFrontPadding);
                DayBox DB = DayBox.GetDay();   // DayBox factory
                DB.DrawDay(DisplayDay);

                Grid.SetRow(DB, i / 7);
                WeekRowGrid.Children.Add(DB);
                Grid.SetColumn(DB, i%7);
            }
        }

        //Generates a calendar for the previous month on button click.
        private void MonthPrev_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            _DispayDate = _DispayDate.AddMonths(-1);
            DrawMonth();
        }

        //Generates a calendar for the next month on button click.
        private void MonthNext_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            _DispayDate = _DispayDate.AddMonths(1);
            DrawMonth();
        }
    }
}

I know I should really be using MVVM but I'm still wrapping my brain around programming in MVVM pattern and want to get this working. I'll probably refactor it once I'm more comfortable with it.

Upvotes: 2

Views: 1800

Answers (2)

Peter Duniho
Peter Duniho

Reputation: 70671

I just wanted to finish this project

Understood. The thing is, the basic idea behind MVVM isn't really all that hard, and if you embrace it, you likely will finish the project faster, than if you continue to try to hard-code all your UI. I can't guarantee that, of course. But I've been through the same thing, and I can tell you that you can spend a lot of time fighting WPF trying to configure the UI in code-behind explicitly.

Without a good Minimal, Complete, and Verifiable code example to start with, it wasn't practical for me to replicate exactly your user interface. But here is a simple code example that shows the basic approach you might take to use MVVM to build the UI you want…

First, it's helpful to have a base class that implements INotifyPropertyChanged for you. It simplifies the view model boilerplate a lot:

class NotifyPropertyChangedBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected void _UpdateField<T>(ref T field, T newValue, [CallerMemberName] string propertyName = null)
    {
        if (!EqualityComparer<T>.Default.Equals(field, newValue))
        {
            field = newValue;
            _OnPropertyChanged(propertyName);
        }
    }

    protected virtual void _OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

Then, we'll need view models. In this UI, there are two basic components: the overall view, and the individual days of the month. So, I made a view model for each:

class DateViewModel : NotifyPropertyChangedBase
{
    private int _dayNumber;
    private bool _isCurrent;

    public int DayNumber
    {
        get { return _dayNumber; }
        set { _UpdateField(ref _dayNumber, value); }
    }

    public bool IsCurrent
    {
        get { return _isCurrent; }
        set { _UpdateField(ref _isCurrent, value); }
    }
}

and…

class MonthViewViewModel : NotifyPropertyChangedBase
{
    private readonly ObservableCollection<DateViewModel> _dates = new ObservableCollection<DateViewModel>();

    private DateTime _selectedDate;

    public DateTime SelectedDate
    {
        get { return _selectedDate; }
        set { _UpdateField(ref _selectedDate, value); }
    }

    public IReadOnlyCollection<DateViewModel> Dates
    {
        get { return _dates; }
    }

    protected override void _OnPropertyChanged(string propertyName)
    {
        base._OnPropertyChanged(propertyName);

        switch (propertyName)
        {
            case nameof(SelectedDate):
                _UpdateDates();
                break;
        }
    }

    private void _UpdateDates()
    {
        _dates.Clear();

        DateTime firstDayOfMonth = new DateTime(SelectedDate.Year, SelectedDate.Month, 1),
            firstDayOfNextMonth = firstDayOfMonth.AddMonths(1);
        int previousMonthDates = (int)firstDayOfMonth.DayOfWeek; // assumes Sunday-start week
        int daysInView = previousMonthDates + DateTime.DaysInMonth(SelectedDate.Year, SelectedDate.Month);

        // round up to nearest week multiple
        daysInView = ((daysInView - 1) / 7 + 1) * 7;

        DateTime previousMonth = firstDayOfMonth.AddDays(-previousMonthDates);

        for (DateTime date = previousMonth; date < firstDayOfNextMonth; date = date.AddDays(1))
        {
            _dates.Add(new DateViewModel { DayNumber = date.Day, IsCurrent = date == SelectedDate.Date });
        }

        for (int i = 1; _dates.Count < daysInView; i++)
        {
            _dates.Add(new DateViewModel { DayNumber = i, IsCurrent = false });
        }
    }
}

As you can see, so far there's been no mention of UI, and yet already all the logic exists to build a month's worth of dates. The UI part, the XAML, will have no idea that you are doing anything related with months or dates. The closest it gets is a hard-coded invariant, i.e. the number of days in a week which are used to control the number of columns in the UniformGrid that will display your data.

The XAML looks like this:

<Window x:Class="TestSO43147585CalendarMonthView.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:p="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:l="clr-namespace:TestSO43147585CalendarMonthView"
        xmlns:s="clr-namespace:System;assembly=mscorlib"
        mc:Ignorable="d"
        SizeToContent="Height"
        Title="MainWindow" Height="350" Width="525">
  <Window.DataContext>
    <l:MonthViewViewModel SelectedDate="{x:Static s:DateTime.Today}"/>
  </Window.DataContext>

  <Window.Resources>
    <DataTemplate DataType="{x:Type l:MonthViewViewModel}">
      <ItemsControl ItemsSource="{Binding Dates}">
        <ItemsControl.ItemsPanel>
          <ItemsPanelTemplate>
            <UniformGrid IsItemsHost="True" Columns="7"/>
          </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>
      </ItemsControl>
    </DataTemplate>

    <DataTemplate DataType="{x:Type l:DateViewModel}">
      <Border BorderBrush="Black" BorderThickness="0, 0, 1, 0">
        <StackPanel>
          <TextBlock Text="{Binding DayNumber}">
            <TextBlock.Style>
              <p:Style TargetType="TextBlock">
                <Setter Property="Background" Value="LightBlue"/>
                <p:Style.Triggers>
                  <DataTrigger Binding="{Binding IsCurrent}" Value="True">
                    <Setter Property="Background" Value="Yellow"/>
                  </DataTrigger>
                </p:Style.Triggers>
              </p:Style>
            </TextBlock.Style>
          </TextBlock>
          <Grid Height="{Binding ActualWidth, RelativeSource={x:Static RelativeSource.Self}}"/>
        </StackPanel>
      </Border>
    </DataTemplate>
  </Window.Resources>

  <Grid>
    <ContentControl Content="{Binding}" VerticalAlignment="Top"/>
  </Grid>
</Window>

The XAML does three things:

  1. It declares a MonthViewViewModel object to be used as the DataContext for the Window. An object in the visual tree, i.e. children of the Window, will inherit their parent's context if they have none of their own.
  2. It declares data templates for the two view models. These tell WPF how it should visually represent the data. A view model contains the data that you want to represent, and this data is referenced in the template via {Binding...} syntax. In many cases (e.g. text, numbers, enum values), you can just bind directly and the default conversion will do what you want (as is the case above). If not, you can implement your own IValueConverter and incorporate that in the binding.
  3. It provides a place for the MonthViewViewModel to be displayed, by declaring a ContentControl, where the content of that control is bound simply to the current data context (a {Binding} expression without a path will bind to the source, and the default source is the current data context).

In the context of the ContentControl, as well as the individual items being displayed in the ItemsControl, WPF will search for the template that is appropriate for the data object defined for that context, and will automatically populate your visual tree, binding to the necessary properties, according to that object.

There are a number of advantages to this approach, the primary ones being that you can describe your UI instead of having to code it, and that you maintain the OOP principle of "separation of concerns", which is key in reducing the mental work-load involved by allowing you to focus on one thing at a time, instead of having to deal with UI and data logic together.

A couple of side-notes regarding the XAML above:

  • You might notice that I have added the p: XML namespace and used it for the Style element. This is only work around a Stack Overflow bug, in which the Style element by itself confuses the XML formatter and prevents the element and its children from being formatted correctly. The XAML will compile fine like this, but it's not actually necessary in real code. In your regular XAML, you can safely omit it.
  • I included a feature your code didn't, just for the purpose of illustration. That is, the current date is highlighted in yellow. The technique shown here is very useful, as it allows you to customize the appearance of an item in a single template, based on property values of the view model. But there is a little trap: in WPF, if you explicitly set an element's property via the attribute syntax, e.g. something like <TextBlock Text="{Binding DayNumber}" Background="LightBlue">, then that syntax will take precedence over any <Setter...> elements in a style. You have to remember to set the default value of any property that you intend to set via a trigger, in its own <Setter...> in the style as well (as shown above).

Upvotes: 2

Daniel Marques
Daniel Marques

Reputation: 703

You could create a method that adjust the number of rows automatically based on the number of weeks (rows). This method always remove all the rows and then add the correct number of rows you need.

private void AdjustRowDefinitions(int numberOfWeeks)
{
    WeekRowGrid.RowDefinitions.Clear();
    for (int i = 0; i < numberOfWeeks; i++)
    {
        RowDefinition rowDef = new RowDefinition();
        rowDef.Height = new GridLength(1, GridUnitType.Star); //this sets the height of the row to *
        WeekRowGrid.RowDefinitions.Add(rowDef);
    }
}

Upvotes: 0

Related Questions