Пишем мессенджер на C#. Часть 1. Вёрстка

Клиент-серверная разработка — одна из самых востребованных отраслей программирования. Зная её азы, можно создавать как мессенджеры, так и онлайн-игры.

В этой серии статей мы напишем клиент-серверное приложение на C# — простейший мессенджер. Серия состоит из трёх частей:

  1. Вёрстка приложения — мы создадим графический интерфейс на C# и XAML для Windows.
  2. Создание WebAPI на ASP.NET — составим базу данных и разработаем серверную часть приложения.
  3. Объединение клиента и сервера — напишем запросы к серверу и позаботимся, чтобы всё работало как надо.

Язык C# пригодится в разработке чего угодно. Возможности WPF (система создания графических интерфейсов) позволяют создавать красивые и функциональные приложения для Windows, а ASP.NET — мощные серверные приложения.

Я постараюсь объяснить подробно, но охватить всё невозможно, поэтому вам нужно знать основы C#, ООП, ASP.NET, WPF и работы в Visual Studio.

Вот несколько статей, с которыми стоит ознакомиться, если вы чего-то не знаете:

Евгений Кучерявый

Пишет о программировании, в свободное время создает игры. Мечтает открыть свою студию и выпускать ламповые RPG.


Структура приложения

Мы не рассматриваем регистрацию, поиск контактов, хранение сообщений, продвинутый дизайн. Вместо этого вы узнаете азы создания клиент-серверных приложений и сможете сделать всё самостоятельно.

Исходный код мессенджера вы найдете на GitHub.

Приложение мы поделим на экраны:

  • экран авторизации;
  • экран с контактами;
  • экран с чатом.

Экран — это элемент Border, который по умолчанию скрыт от пользователя. Виден будет только активный экран.

На экране авторизации пользователь сможет ввести логин и пароль, чтобы войти. Если он ввёл верные данные, то перейдёт на экран с контактами, иначе — увидит сообщение об ошибке.

На экране с контактами видны имена других пользователей, с которыми ведётся переписка. Чат открывается при нажатии на имя другого пользователя.

На экране с чатом видна переписка с одним конкретным контактом. Пользователь может написать и отправить новое сообщение или вернуться к экрану с контактами.

Верстаем экран авторизации

Начнём с определения стилей. В них минимально обозначим, как должны выглядеть элементы, и сразу укажем, что экраны по умолчанию должны быть скрыты:

<Style x:Key="Screen">
    <Style.Setters>
   	 <Setter Property="Border.Visibility" Value="Hidden" />
   	 <Setter Property="Border.Background" Value="#151515" />
    </Style.Setters>
</Style>

<Style x:Key="LoginPanel">
    <Style.Setters>
   	 <Setter Property="StackPanel.Orientation" Value="Vertical" />
   	 <Setter Property="StackPanel.VerticalAlignment" Value="Center" />
    </Style.Setters>
</Style>

<Style x:Key="TextBoxBase">
    <Style.Setters>
   	 <Setter Property="TextBox.Background" Value="#333" />
   	 <Setter Property="TextBox.Foreground" Value="#f6f6f6" />
   	 <Setter Property="TextBox.Margin" Value="5"/>
   	 <Setter Property="TextBox.Padding" Value="15 10"/>
   	 <Setter Property="TextBox.HorizontalAlignment" Value="Center" />
   	 <Setter Property="TextBox.Width" Value="250" />
    </Style.Setters>
</Style>

<Style x:Key="ButtonBase" >
    <Style.Setters>
   	 <Setter Property="Button.Background" Value="#333" />
   	 <Setter Property="Button.Foreground" Value="#f6f6f6" />
   	 <Setter Property="Button.Margin" Value="5"/>
   	 <Setter Property="Button.Padding" Value="50 10"/>
   	 <Setter Property="Button.HorizontalAlignment" Value="Center" />
   	 <Setter Property="Button.FontSize" Value="14" />
    </Style.Setters>
</Style>

Теперь сверстаем сам экран авторизации — он должен быть видимым:

<Border Style="{StaticResource Screen}" Name="LoginScreen" Visibility="Visible">
    <StackPanel Style="{StaticResource LoginPanel}">
   	 <TextBlock Text="Login" Style="{StaticResource HeaderBlock}" />
   	 <TextBox Style="{StaticResource TextBoxBase}" Name="LoginBox" />
   	 <PasswordBox Style="{StaticResource TextBoxBase}" Name="PasswordBox"/>
   	 <Button Content="Enter" Style="{StaticResource ButtonBase}" Name="LoginButton" Click="LoginButton_Click" IsDefault="True"/>
   	 <TextBlock Text="" Name="LoginMessageBlock" Style="{StaticResource WarningBlock}" Visibility="Hidden"/>
    </StackPanel>
</Border>

Давайте посмотрим, как это выглядит:

Теперь напишем обработчик для кнопки Enter:

//Обработчик нажатия на кнопку Login
private void LoginButton_Click(object sender, RoutedEventArgs e)
{
    //Пока используем тестовые данные
    if(LoginBox.Text == "admin" && PasswordBox.Password == "12345")
    {
   	 //Если логин и пароль верные, то переходим на другой экран
   	 Open(ContactsScreen);
    }
    else
    {
   	 //Иначе выводим сообщение об ошибке авторизации
   	 LoginMessageBlock.Text = "Wrong login or password!";
   	 LoginMessageBlock.Visibility = Visibility.Visible;
    }
}
 
//Метод для открытия другого экрана
private void Open(Border screen)
{
    //Делаем все экраны невидимыми
    LoginScreen.Visibility = Visibility.Hidden;
    ContactsScreen.Visibility = Visibility.Hidden;
    ChatScreen.Visibility = Visibility.Hidden;
 
    //Делаем видимым необходимый экран
    screen.Visibility = Visibility.Visible;
}

У вас должны быть экраны с именами ContactsScreen и ChatScreen, чтобы метод Open () работал корректно. Для этого достаточно создать два пустых элемента Border.

Вот как выглядит экран авторизации при вводе неверных данных:

Дальше сверстаем экран с контактами.

Верстаем экран с контактами

Экран с контактами разделим на два ряда с помощью Grid — заголовок в первом ряду и список контактов во втором. Список — это элемент ListBox, в котором перечислены контакты.

<Border Name="ContactsScreen" Style="{StaticResource Screen}">
    <Grid>
   	 <Grid.RowDefinitions>
   		 <RowDefinition Height="50" />
   		 <RowDefinition />
   	 </Grid.RowDefinitions>
 
   	 <Border Grid.Row="0" Style="{StaticResource HeaderBorder}">
   		 <TextBlock Style="{StaticResource HeaderBlock}" Text="Contacts" VerticalAlignment="Center"/>
   	 </Border>
   	 
   	 <Border Grid.Row="1">
   		 <ListBox ScrollViewer.VerticalScrollBarVisibility="Auto" ScrollViewer.CanContentScroll="True"  
   					 Style="{StaticResource ContactsList}" Name="ContactsList" SelectionChanged="ContactsList_SelectionChanged">
   			 <ListBox.ItemTemplate>
   				 <DataTemplate>
   					 <ListBoxItem>
   						 <DockPanel LastChildFill="True">
   							 <Image Style="{StaticResource ContactImage}" DockPanel.Dock="Left"></Image>
   							 <TextBlock Text="{Binding Name}" Style="{StaticResource ContactName}" DockPanel.Dock="Right"/>
   						 </DockPanel>
   					 </ListBoxItem>
   				 </DataTemplate>
   			 </ListBox.ItemTemplate>
   		 </ListBox>
   	 </Border>
    </Grid>
</Border>

Здесь описаны не все элементы списка, а только шаблон для них, который будет использоваться во время привязки данных. Этот шаблон понадобится в готовом приложении, но для удобства вёрстки вы можете подготовить тестовые данные.

Зададим стили:

<Style x:Key="ContactsList" BasedOn="{StaticResource TextBlockBase}">
    <Style.Setters>
   	 <Setter Property="ListBox.Background" Value="#151515"/>
   	 <Setter Property="ListBox.BorderThickness" Value="0"/>
    </Style.Setters>
</Style>
 
<Style x:Key="ContactImage" BasedOn="{StaticResource TextBlockBase}">
    <Style.Setters>
   	 
    </Style.Setters>
</Style>
 
<Style x:Key="ContactName" BasedOn="{StaticResource TextBlockBase}">
    <Style.Setters>
   	 <Setter Property="TextBlock.HorizontalAlignment" Value="Left"/>
    </Style.Setters>
</Style>
 
<Style x:Key="HeaderBorder">
    <Style.Setters>
   	 <Setter Property="Border.Background" Value="#222" />
    </Style.Setters>
</Style>

Теперь остаётся написать метод, который открывает чат с контактом. Этот метод вызывается, когда пользователь выберет пункт из списка.

private void ContactsList_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    //Метод вызывается, когда меняется индекс выделенного элемента
    //При выделении элемент списка будет подсвечиваться
    //Чтобы убрать это, мы будем менять индекс на -1
    //Чтобы метод не срабатывал повторно, мы проверяем, чтобы индекс был больше или равен 0
    if(ContactsList.SelectedIndex >= 0)
    {
   	 //Тут будет код загрузки сообщений из чата
 
   	 //Сбрасываем индекс
   	 ContactsList.SelectedIndex = -1;
 
   	 Open(ChatScreen);
    }
}

Верстаем экран с чатом

Чат похож на экран с контактами, но немного дополненный:

Grid делит экран на три части: заголовок, чат и поле ввода. В заголовке — имя собеседника и кнопка «Назад». В чате выведены сообщения с помощью ListBox, а внизу находятся поле ввода и кнопка отправки.

<Border Name="ChatScreen" Style="{StaticResource Screen}">
    <Grid>
   	 <Grid.RowDefinitions>
   		 <RowDefinition Height="50" />
   		 <RowDefinition />
   		 <RowDefinition Height="50"/>
   	 </Grid.RowDefinitions>
 
   	 <Border Grid.Row="0" Style="{StaticResource HeaderBorder}">
   		 <Grid>
    			 <Grid.ColumnDefinitions>
   				 <ColumnDefinition Width="1*" />
   				 <ColumnDefinition Width="6*" />
   				 <ColumnDefinition Width="1*" />
   			 </Grid.ColumnDefinitions>
 
   			 <Button Style="{StaticResource NavButton}" Grid.Column="0" Name="BackButton" Content="←" Click="BackButton_Click"/>
 
   			 <TextBlock Style="{StaticResource HeaderBlock}" Text="" VerticalAlignment="Center" Name="ChatName" Grid.Column="1"/>
   			 
   			 
   		 </Grid>
   		 
   	 </Border>
 
   	 <Border Grid.Row="1">
   		 <ListBox ScrollViewer.VerticalScrollBarVisibility="Auto" ScrollViewer.CanContentScroll="True"
   					 Style="{StaticResource ContactsList}" Name="MessagesList" Focusable="False"
   					 HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch">
   			 <ListBox.ItemTemplate>
   				 <DataTemplate>
   					 <ListBoxItem>
   						 <Border Style="{StaticResource MessageBorder}" HorizontalAlignment="{Binding Alignment}">
   							 <StackPanel Orientation="Vertical">
   								 <TextBlock Text="{Binding Text}" Style="{StaticResource MessageText}"/>
   								 <TextBlock Text="{Binding Date}" Style="{StaticResource MessageDate}"/>
   							 </StackPanel>
   						 </Border>
   					 </ListBoxItem>
   				 </DataTemplate>
   			 </ListBox.ItemTemplate>
   		 </ListBox>
   	 </Border>
 
   	 <Border Grid.Row="2" Style="{StaticResource HeaderBorder}">
   		 <Grid>
 
   			 <Grid.ColumnDefinitions>
   				 <ColumnDefinition Width="6*" />
   				 <ColumnDefinition Width="1*" />
   			 </Grid.ColumnDefinitions>
 
   			 <TextBox Name="MessageBox" Style="{StaticResource MessageBox}" Grid.Column="0"/>
   			 
   			 <Button Style="{StaticResource NavButton}" Grid.Column="1" Name="SendButton" Content="→" Click="SendButton_Click"/>
 
   		 </Grid>
   	 </Border>
    </Grid>
</Border>

Тут, как на экране с контактами, пока нет данных -— только шаблон для их вывода. Немного стилей:

<Style x:Key="MessageBorder">
    <Style.Setters>
   	 <Setter Property="Border.Background" Value="#555" />
   	 <Setter Property="Border.CornerRadius" Value="13" />
   	 <Setter Property="Border.MinWidth" Value="100" />
   	 <Setter Property="Border.MaxWidth" Value="300" />
   	 <Setter Property="Border.Padding" Value="2" />
    </Style.Setters>
</Style>
 
<Style x:Key="MessageText" BasedOn="{StaticResource TextBlockBase}">
    <Style.Setters>
   	 <Setter Property="TextBlock.TextWrapping" Value="Wrap" />
   	 <Setter Property="TextBlock.Margin" Value="0" />
    </Style.Setters>
</Style>
 
<Style x:Key="MessageDate" BasedOn="{StaticResource TextBlockBase}">
    <Style.Setters>
   	 <Setter Property="TextBlock.HorizontalAlignment" Value="Right" />
   	 <Setter Property="TextBlock.FontSize" Value="8" />
   	 <Setter Property="TextBlock.Margin" Value="0" />
    </Style.Setters>
</Style>

Остаётся только обработать события для кнопок отправки и навигации:

private void SendButton_Click(object sender, RoutedEventArgs e)
{
 
    string text = "";
 
    if(!string.IsNullOrEmpty(MessageBox.Text))
    {
   	 
   	 text = MessageBox.Text.Trim();
    }
 
    if(!string.IsNullOrEmpty(text))
    {
 
   	 bool result = true;
 
   	 if(result)
   	 {
   		 MessageBox.Text = "";
   	 }
   	 
    }    
}
 
private void BackButton_Click(object sender, RoutedEventArgs e)
{
    Open(ContactsScreen);
}

Заключение

У нас получился довольно минималистичный дизайн, который вы можете доработать как вам нравится. Дальше нам предстоит написать серверную часть приложения, а потом связать их вместе.

Если вы хотите научиться профессионально разрабатывать приложения на мощном и удобном языке C#, то обратите внимание на наш курс. В ходе обучения вы напишете несколько полноценных проектов, которые станут отличным началом для вашего портфолио.

Курс

Профессия C#-разработчик


130 часов — и вы научитесь писать программы на языке, созданном Microsoft. Вы создадите 5 проектов для портфолио, даже если до этого никогда не программировали. После прохождения обучения — помощь в трудоустройстве.

Хочешь получать крутые статьи по программированию?
Подпишись на рассылку Skillbox