WPF
April 13, 2019

Проект "Отпуск сотрудников". Часть 3, интерфейс пользователя.


!!НАШ БЛОГ ПЕРЕЕХАЛ!!

Мы создали свой сайт! Все материалы, опубликованные в этом блоге, переехали туда.

Наш новый сайт maddevelop.ru


Клиентскую часть реализуем с помощью технологии WPF, воспользовавшись паттерном MVVM. Модели будут те же, что и в серверной части. Просто скопируем их в созданную папку Models. Создадим класс MainViewModel, в котором опишем взаимодействие с API контроллером и с интерфейсом пользователя. Большинство свойств класса модели представления реализуют метод OnPropertyChanged(), поэтому изменение свойства приводит к изменению элемента управления, к которому оно привязано.

public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged([CallerMemberName]string prop = "")
{
    if (PropertyChanged != null)
        PropertyChanged(this, new PropertyChangedEventArgs(prop));
}  

Окно пользователя будет выглядеть следующим образом:

Цифрами обозначены элементы управления для удобства дальнейшего описания. В таблице ниже удобно посмотреть, как организована привязка свойства класса MainViewModel к свойству элемента управления пользовательского интерфейса.


К свойству Command кнопок следует привязать свойство класса RelayCommand в модели представления.

public class RelayCommand : ICommand
{
    private Action<object> execute;
    private Func<object, bool> canExecute;

    public event EventHandler CanExecuteChanged
    {
        add { CommandManager.RequerySuggested += value; }
        remove { CommandManager.RequerySuggested -= value; }
    }

    public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null)
    {
        this.execute = execute;
        this.canExecute = canExecute;
    }

    public bool CanExecute(object parameter)
    {
        return this.canExecute == null || this.canExecute(parameter);
    }

    public void Execute(object parameter)
    {
        this.execute(parameter);
    }
}

Вторая таблица в пользовательском интерфейсе состоит из четырёх: по одной для каждого квартала года. Для того, чтобы к ним можно было привязывать свойства типа двумерных массивов, следует установить пакет NuGet "Gu.Wpf.DataGrid2D". Эти матрицы составим из объектов типа Cell.

public Cell[,] FirstQuarter { get; set; }
public class Cell
{
    public Color Color { get; }

    public Cell(Color color)
    {            
        Color = color;
    }
}

Выделение отпусков сотрудников цветом возможно, если к фону каждой ячейки привязать через конвертер значений свойство Color класса Cell.

public class ConvMycolorColor : IValueConverter
{
    private static System.Drawing.Color color;

    public object Convert(object value, Type targetType, object parameter, 
        CultureInfo culture)
    {
        color = System.Drawing.Color.FromArgb(((Models.Color)value).ColorNumber);
        return new SolidColorBrush(System.Windows.Media.Color
            .FromArgb(color.A, color.R, color.G, color.B));
    }

    public object ConvertBack(object value, Type targetType, object parameter, 
        CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

Запишем для ясности определение всех полей и свойств класса MainViewModel.

public class MainViewModel : INotifyPropertyChanged
{
    private static IEnumerable<Employee> employees;        
    private static HttpClient client = new HttpClient();                           
    private StringBuilder errorSB = new StringBuilder();

    // Привязанное свойство №1 из таблицы выше
    private string name;
    public string Name
    {
        get => name;
        set
        {
            name = value;
            OnPropertyChanged(nameof(Name));
        }
    }

    // №2
    public IEnumerable<Color> Colors { get; set; }

    // Также для элемента управления №2
    private Color empColor;
    public Color EmpColor
    {
        get => empColor;
        set
        {
            empColor = value;
            OnPropertyChanged(nameof(empColor));
        }
    }

    // №3
    public RelayCommand AddEmployee { get; set; }
    
    // №4    
    public RelayCommand DeleteEmployee { get; set; }

    // №8
    public RelayCommand CommandAddVacation { get; set; }

    // №9
    public RelayCommand CommandDeleteVacation { get; set; }  

    // №15
    public RelayCommand CommandRefresh { get; set; }
    
    // №5
    private Employee currentEmployee;
    public Employee CurrentEmployee
    {
        get => currentEmployee;
        set
        {
            currentEmployee = value;
            OnPropertyChanged(nameof(CurrentEmployee));
        }
    }

    // Привязываемое свойство к свйоству также элемента №5
    private Vacation currentVacation;
    public Vacation CurrentVacation
    {
        get => currentVacation;
        set
        {
            currentVacation = value;
            OnPropertyChanged(nameof(CurrentVacation));
            if (currentVacation != null)
            {
                Start = currentVacation.Start;
                Duration = currentVacation.Duration.ToString();
            }                
        }
    }

    // №6
    private DateTime start = DateTime.Now;
    public DateTime Start
    {
        get => start;
        set
        {
            start = value;
            OnPropertyChanged(nameof(Start));
        }
    }

    // №7
    private string duration;
    public string Duration
    {
        get => duration;
        set
        {
            duration = value;
            OnPropertyChanged(nameof(Duration));
        }
    }

    // №10
    private DataView table;
    public DataView Table
    {
        get => table;
        set
        {
            table = value;                             
            OnPropertyChanged(nameof(Table));
            EmployeeNames = employees.Select(x => x.Name).ToList();                
        }
    }

    // №10
    private DataRowView currentRow;
    public DataRowView CurrentRow
    {
        get => currentRow;
        set
        {
            currentRow = value;
            OnPropertyChanged(nameof(CurrentRow));
            if (currentRow != null)
            {
                CurrentEmployee = employees
                    .FirstOrDefault(e => e.Name 
                        == currentRow.Row.ItemArray.ElementAt(0) as string);
                Name = CurrentEmployee.Name;
                EmpColor = Colors.FirstOrDefault(c => CurrentEmployee.ColorId == c.ColorId);
            }                    
        }
    }

    // №11
    private IEnumerable<string> employeeNames;
    public IEnumerable<string> EmployeeNames
    {
        get => employeeNames;
        set
        {
            employeeNames = value;
            OnPropertyChanged(nameof(EmployeeNames));
        }
    }
    
    // №11    
    public Cell[,] FirstQuarter { get; set; }

    // №12
    public Cell[,] SecondQuarter { get; set; }

    // №13
    public Cell[,] ThirdQuarter { get; set; }

    // №14
    public Cell[,] FourthQuarter { get; set; }

    // №16
    private string error;
    // Свойство для записывания ошибок
    public string Error
    {
        get => error;
        set
        {
            error = value;
            OnPropertyChanged(nameof(Error));
        }
    }       

Напоследок приведём XAML-разметку окна программы:

<Window x:Class="FrontendWPF.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:dataGrid2D="http://gu.se/DataGrid2D" 
        xmlns:local="clr-namespace:FrontendWPF"
        mc:Ignorable="d"
        Title="MainWindow" SizeToContent="WidthAndHeight">
    <Window.DataContext>
        <local:MainViewModel/>
    </Window.DataContext>
    <Window.Resources>
        <local:ConvMyColorString x:Key="myColorConverter"/>
        <local:ConvMycolorColor x:Key="colorToTrueColor"/>
    </Window.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
            <RowDefinition/>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Grid>            
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition/>
            </Grid.ColumnDefinitions>
            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition/>
                    <RowDefinition/>
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="50"/>
                    <ColumnDefinition Width="200"/>
                    <ColumnDefinition Width="85"/>
                    <ColumnDefinition Width="25"/>
                    <ColumnDefinition Width="90"/>
                    <ColumnDefinition Width="90"/>
                    <ColumnDefinition Width="110"/>
                    <ColumnDefinition Width="85"/>
                </Grid.ColumnDefinitions>
                <TextBlock Text="Отпуски" Grid.Column="4" Margin="5" 
                    HorizontalAlignment="Center" VerticalAlignment="Bottom"/>
                <ComboBox Grid.Column="4" Grid.Row="1" Margin="5" Width="80" 
                    HorizontalAlignment="Left" SelectedItem="{Binding CurrentVacation}" 
                    ItemStringFormat="d" ItemsSource="{Binding CurrentEmployee.Vacations}" 
                    DisplayMemberPath="Start"/>
                <TextBlock Text="Дата начала:" Margin="5" Grid.Column="5" 
                    HorizontalAlignment="Right"/>
                <TextBlock Text="Длительность:" Margin="5" Grid.Column="5" Grid.Row="1" 
                    HorizontalAlignment="Right"/>
                <DatePicker Grid.Column="6" Margin="5" SelectedDateFormat="Short" 
                    SelectedDate="{Binding Start}" DisplayDateStart="2019/01/01" 
                    DisplayDateEnd="2019/12/31"/>
                <TextBox Grid.Column="6" Grid.Row="1" Margin="5" Width="22" 
                    HorizontalAlignment="Left" 
                    Text="{Binding Duration, UpdateSourceTrigger=PropertyChanged}"/>
                <Button Content="Добавить" Grid.Column="7" Width="75" Margin="5" 
                    Command="{Binding CommandAddVacation}"/>
                <Button Content="Удалить" Grid.Column="7" Grid.Row="1" Width="75" Margin="5" 
                    Command="{Binding CommandDeleteVacation}"/>
                <TextBlock Text="ФИО:" Margin="5" HorizontalAlignment="Right"/>
                <TextBox Grid.Column="1" Margin="5" 
                    Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}"/>
                <TextBlock Text="Цвет:" Grid.Row="1" Margin="5" HorizontalAlignment="Right"/>
                <ComboBox Grid.Column="1" Grid.Row="1" Margin="5" Width="80" 
                    HorizontalAlignment="Left" SelectedItem="{Binding EmpColor}"
                    ItemsSource="{Binding Colors}">
                    <ComboBox.ItemTemplate>
                        <DataTemplate>
                            <TextBlock Text="{Binding Converter={StaticResource 
                                myColorConverter}}"/>
                        </DataTemplate>
                    </ComboBox.ItemTemplate>
                </ComboBox>
                <Button Content="Добавить" Grid.Column="2" Width="75" Margin="5" 
                    Command="{Binding AddEmployee}"/>
                <Button Content="Удалить" Grid.Column="2" Grid.Row="1" Width="75" Margin="5" 
                    Command="{Binding DeleteEmployee}"/>
            </Grid>            
        </Grid>

        <DataGrid ItemsSource="{Binding Table}" CanUserAddRows="False" 
            HorizontalAlignment="Left" Grid.Row="1" SelectedItem="{Binding CurrentRow}"/>
        
        <TextBlock Grid.Row="2" Text="График отпусков на 2019 год" FontWeight="Bold" 
            Margin="5,20,5,5"/> 
        
        <StackPanel Grid.Row="3" Orientation="Horizontal" Margin="5">
            <StackPanel>
                <TextBlock Text="1 квартал" HorizontalAlignment="Center"/>
                <DataGrid SelectionUnit="Cell" ColumnWidth="3"
                    dataGrid2D:ItemsSource.Array2D="{Binding FirstQuarter, 
                    UpdateSourceTrigger=PropertyChanged}" HeadersVisibility="Row" 
                    MinColumnWidth="2" GridLinesVisibility="Horizontal" 
                    HorizontalAlignment="Left" dataGrid2D:ItemsSource.RowHeadersSource=
                    "{Binding EmployeeNames, UpdateSourceTrigger=PropertyChanged}">
                    <DataGrid.Resources>
                        <Style TargetType="DataGridCell">
                            <Setter Property="BorderThickness" Value="0"/>
                        </Style>
                    </DataGrid.Resources>
                    <dataGrid2D:Cell.Template>
                        <DataTemplate>
                            <Grid Background="{Binding Color, Converter={StaticResource 
                                colorToTrueColor}}" Width="3"/>
                        </DataTemplate>
                    </dataGrid2D:Cell.Template>
                </DataGrid>
            </StackPanel>
            
            <StackPanel>
                <TextBlock Text="2 квартал" HorizontalAlignment="Center"/>
                <DataGrid SelectionUnit="Cell" dataGrid2D:ItemsSource.Array2D=
                    "{Binding SecondQuarter, UpdateSourceTrigger=PropertyChanged}"  
                    ColumnWidth="3" HeadersVisibility="None"  MinColumnWidth="2" 
                    GridLinesVisibility="Horizontal" HorizontalAlignment="Left" 
                    RowHeight="22">
                    <DataGrid.Resources>
                        <Style TargetType="DataGridCell">
                            <Setter Property="BorderThickness" Value="0"/>
                        </Style>
                    </DataGrid.Resources>
                    <dataGrid2D:Cell.Template>
                        <DataTemplate>
                            <Grid Background="{Binding Color, Converter={StaticResource 
                                colorToTrueColor}}" Width="3"/>
                        </DataTemplate>
                    </dataGrid2D:Cell.Template>
                </DataGrid>
            </StackPanel>


            <StackPanel>
                <TextBlock Text="3 квартал" HorizontalAlignment="Center"/>
                <DataGrid SelectionUnit="Cell" dataGrid2D:ItemsSource.Array2D="{Binding 
                    ThirdQuarter, UpdateSourceTrigger=PropertyChanged}" ColumnWidth="3" 
                    HeadersVisibility="None"  MinColumnWidth="2" RowHeight="22"
                    GridLinesVisibility="Horizontal" РorizontalAlignment="Left">
                    <DataGrid.Resources>
                        <Style TargetType="DataGridCell">
                            <Setter Property="BorderThickness" Value="0"/>
                        </Style>
                    </DataGrid.Resources>
                    <dataGrid2D:Cell.Template>
                        <DataTemplate>
                            <Grid Background="{Binding Color, Converter={StaticResource 
                                colorToTrueColor}}" Width="3"/>
                        </DataTemplate>
                    </dataGrid2D:Cell.Template>
                </DataGrid>
            </StackPanel>


            <StackPanel>
                <TextBlock Text="4 квартал" HorizontalAlignment="Center"/>
                <DataGrid SelectionUnit="Cell" dataGrid2D:ItemsSource.Array2D="{Binding 
                    FourthQuarter, UpdateSourceTrigger=PropertyChanged}" ColumnWidth="3" 
                    HeadersVisibility="None"  MinColumnWidth="2" RowHeight="22"
                    GridLinesVisibility="Horizontal" HorizontalAlignment="Left">
                    <DataGrid.Resources>
                        <Style TargetType="DataGridCell">
                            <Setter Property="BorderThickness" Value="0"/>
                        </Style>
                    </DataGrid.Resources>
                    <dataGrid2D:Cell.Template>
                        <DataTemplate>
                            <Grid Background="{Binding Color, Converter={StaticResource 
                                colorToTrueColor}}" Width="3"/>
                        </DataTemplate>
                    </dataGrid2D:Cell.Template>
                </DataGrid>
            </StackPanel>
        </StackPanel>          
 
        <StackPanel Grid.Row="4" Orientation="Horizontal">
                <Button Content="Обновить" Width="80" Command="{Binding CommandRefresh}" 
                    HorizontalAlignment="Right" Margin="5,0,20,5" 
                    VerticalAlignment="Center"/>
                <TextBlock Text="Ошибки:" Margin="5,0,0,5" VerticalAlignment="Center"/>
                <TextBox Width="300" Margin="5,0,5,5" Text="{Binding Error}"/>
        </StackPanel>           
    </Grid>
</Window>

В следующей части рассмотрим, каким образом происходит заполнение двух таблиц в интерфейсе пользователя.


Ещё больше интересной информации на нашем Telegram-канале.

<< К части 2 << .........