Thursday, November 27, 2014

Динамическая загрузка View в Prism 5

Сегодня мы снова коснемся темы использования регионов в Prism 5. В прошлой теме я затронул использование View Discovery и View Injection в Prism 5. Как я и предполагал, это вызвало вопрос: "Что делать, если нужно в конкретный регион подгрузить представление (view) динамически?". Именно это и будет сегодняшней темой данной статьи, а именно: один из способов динамического управления представлениями с помощью внедрения зависимостей (view injection). Мы не будем злоупотреблять теорией и построим самый пример, так как основной целью является показать, что вы можете без особого труда использовать это в своих программах. В следующих статьях рассмотрим более сложный способ навигации по регионам и управление ими с помощью View-Switching Navigation, хотя мне больше по душе старое название данного типа навигации в Prism 5 – View Based Navigation. Существует два способа работы с представлениями в Prism 5:
  • обнаружение представлений (View Discovery), которое позволяет при старте автоматически указать, как связать конкретный регион с нужным представлением;
  • внедрение представлений (View Injection), которое позволяет программно связать представление с регионом как при старте программы та и динамически во время работы программы.
Как говорят в народе, “меньше слов, больше дела”; приступим к реализации. Итак, что же будет собой представлять наша программа. Это будет приложение, в котором мы создадим в главном окне регион “MainRegion”, а также отдельно два UserControl, один из которых будет загружен при старте программы в наш регион, а второй будет загружен динамически по нажатию кнопки в первом контроле. Вроде ничего сложного нет, поэтому приступаем к реализации. Для этого заходим в Visual Studio и создаем новый WPF проект с выбором фреймворка 4.5 для того чтобы использовать призм 5-й версии. Назовем наш проект “DynamicViewInjection”, чтобы он отражал суть нашей работы.
Ниже на рисунке показано стартовое окно со всеми настройками. Затем сразу через Manager NuGet Packages загрузим Prism и Prism.UnityExtensions, для того чтобы в качестве загрузчика приложения использовать UnityBootstrapper, который работает совместно с IoC контейнером Unity.
Ребята с Patterns & Practices старались так сильно все разбить, чтобы библиотеки Prism 5 можно было использовать по отдельности, что это привело к тому, что если мы ставим сразу данную библиотеку целиком и плюс к этому ставим UnityBootstrapper загрузчик, то в итоге получим ни много ни мало 11 библиотек.
Лично мне не очень нравится такое огромное количество библиотек. Но если учесть, что в Prism 5 есть практически все для построения корпоративных приложений, то такое количество может быть оправдано. Я немного отошел от темы, поэтому вернемся к разметке нашего проекта. Поскольку с Prism 5 использование MVVM – это,по сути, просто необходимость, иначе если вы не используете MVVM, тогда не совсем понятно, зачем вам вообще нужен Prism. Это я веду к тому, что структура папок будет разбита согласно концепции Prism + MVVM. У меня это выглядит так:
Чтобы не расписывать отдельно каждую папку, я решил сделать это в виде рисунка. Поскольку мы работаем в призме, нам необходимо и оперировать терминами призма. Главное окно в Prism 5 называется оболочкой Shell. Поэтому мы перенесем наше окно сразу в папку Views и переименуем его с “MainWindow” на “Shell”. После этого нужно перейти в разметку App.xaml и удалить StartupUri, который запускает главное окно. Главное окно у нас будет запускаться с помощью загрузчика (bootstrapper) призма. Для этого мы сделаем небольшую заготовку, в которой дальше вставим код запуска нашего bootstrapper. Переходим в файл App.xam.cs и переопределяем метод OnStartup.
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);
    }
}
Если вы все проделали правильно, как минимум ваш проект должен собраться. Если что-то не получилось, то, в принципе, ничего страшного. Просто откройте эту статью: "Введение в Prism 5. Bootstrapper" и шаг за шагом делайте то, что там прописано. Теперь наша задача – добавить загрузчик, в котором мы пропишем инициализацию нашего приложения. Для этого в проект добавим новый класс Bootstrapper.cs. Реализация этого класса приведена ниже.
public class Bootstrapper : UnityBootstrapper
{
    protected override DependencyObject CreateShell()
    {
        return Container.Resolve<Shell>();
    }

    protected override void InitializeShell()
    {
        Application.Current.MainWindow = (Window)Shell;
        Application.Current.MainWindow.Show();
    }
}
Затем переходим в наш класс App.xaml.cs и добавляем в заготовку метода OnStartup следующий код:
protected override void OnStartup(StartupEventArgs e)
{
    var bootstrapper = new Bootstrapper();
    bootstrapper.Run();
}
После этого, если вы запустите проект, то должны увидеть на экране пустое окно.
Теперь самое время приступить к реализации контролов, которые будут заполнять наш регион. Для начала сделаем разметку в нашей оболочке Shell.xaml. В ней нет ничего сложного, просто ContentControl, который будет отображать наше содержимое.
<Window x:Class="DynamicViewInjection.Shell"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:regions="http://www.codeplex.com/CompositeWPF"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <ContentControl regions:RegionManager.RegionName="MainRegion" />
    </Grid>
</Window>
Затем переходим в папку Regions и добавляем туда новый UserControl с именем FirstView. Реализация этого окна приведена ниже.
<UserControl x:Class="DynamicViewInjection.Views.Regions.FirstView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             mc:Ignorable="d"
             d:DesignHeight="300" d:DesignWidth="300">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="50"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>

        <Border Background="Gold" Margin="2" Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="0">
            <StackPanel Orientation="Horizontal">
                <Button Content="Button1" Margin="3,2"/>
                <Button Content="Button2" Margin="3,2"/>
            </StackPanel>
        </Border>

        <Grid Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="1" Background="ForestGreen">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="*" />
            </Grid.ColumnDefinitions>
            <ComboBox Grid.Column="0" />
        </Grid>

        <StackPanel Grid.Column="0"  Grid.Row="2" Background="DodgerBlue" Margin="5,3"/>
        <StackPanel Grid.Column="1"  Grid.Row="2" Background="DodgerBlue" Margin="5,3"/>

        <StackPanel Grid.Column="0"  Grid.Row="3" Grid.ColumnSpan="2" Background="Firebrick" Margin="5,10"/>
    </Grid>
</UserControl>
Внешний вид контрола приведен ниже.
Я старался эмулировать окно, как в реальном приложении, поэтому цветами выделил те области, вместо которых при нормальной разметке можно использовать регионы для заполнения.
Ну и добавим второй контрол, с названием которого тоже не будем сильно изобретать  и назовем его просто: SecondView.
<UserControl x:Class="DynamicViewInjection.Views.Regions.SecondView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             mc:Ignorable="d"
             d:DesignHeight="300" d:DesignWidth="300">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>

        <Grid Grid.Row="0" Background="ForestGreen">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="*" />
            </Grid.ColumnDefinitions>
            <ComboBox Grid.Column="0" />
        </Grid>

        <StackPanel Grid.Column="0"  Grid.Row="1" Background="DodgerBlue" Margin="5,3"/>

        <Border Background="Gold" Margin="2" Grid.Row="1" Height="25" HorizontalAlignment="Left" VerticalAlignment="Top">
            <StackPanel Orientation="Horizontal">
                <Button Content="Button1" Margin="3,2"/>
                <Button Content="Button2" Margin="3,2"/>
                <Button Content="Button3" Margin="3,2"/>
            </StackPanel>
        </Border>
    </Grid>
</UserControl>
В этом контроле для разнообразия я добавил три кнопки, чтобы показать, что это другое представление.
Теперь сделаем так, чтобы первый контрол по умолчанию загружался в наш главный регион, который мы добавили в Shell.xaml с названием “MainRegion”. Для этого нам необходимо перейти в папку Modules и добавить новый класс MainModule.cs.
public class MainModule : IModule
{
    private readonly IRegionManager _regionManager;
    public readonly IUnityContainer _container;

    public MainModule(IUnityContainer container, IRegionManager regionManager)
    {
        _container = container;
        _regionManager = regionManager;
    }

    public void Initialize()
    {
        _regionManager.Regions["MainRegion"].Add(_container.Resolve<FirstView>());
    }
}
Теперь для того чтобы это чудо заработало, нам необходимо добавить инициализацию нашего модуля в созданный ранее бутстраппер. В нем добавится всего лишь один переопределённый метод InitializeModules.
public class Bootstrapper : UnityBootstrapper
{
    protected override DependencyObject CreateShell()
    {
        return Container.Resolve<Shell>();
    }

    protected override void InitializeShell()
    {
        Application.Current.MainWindow = (Window)Shell;
        Application.Current.MainWindow.Show();
    }

    protected override void InitializeModules()
    {
        IModule mainModule = Container.Resolve<MainModule>();
        mainModule.Initialize();
    }
}
Теперь запустим наш проект, чтобы посмотреть, что наше окно отображает уже заполненный регион. Теперь пришло время для написания логики. Мы сделаем так, чтобы нажатие на первую кнопку контрола FirstView загружало в регион контрол SecondView, а нажатие на вторую кнопку в SecondView, наоборот, загружало FirstView. То есть, контролы загружают друг друга. Для этого нам понадобится реализовать модель представления. Для этого нужно перейти в папку ViewModels и добавить класс FirstViewModel для имплементации логики FirstView, и аналогично − класс SecondViewModel для второго контрола. Ниже приведена реализация FirstViwModel.
public class FirstViewModel : BindableBase
{
    #region Private Variables
    private readonly IRegionManager _regionManager;
    private readonly IUnityContainer _unityContainer;
    #endregion

    #region Constructor
    public FirstViewModel(IRegionManager regionMananger, IUnityContainer container)
    {
        _regionManager = regionMananger;
        _unityContainer = container;
        ButtonCommand = new DelegateCommand(ShowSecondView);
    }
    #endregion

    #region Commands
    public DelegateCommand ButtonCommand { get; private set; }

    #endregion

    #region Private Methods
    private void ShowSecondView()
    {
        var view = _unityContainer.Resolve<SecondView>();
        view.DataContext = _unityContainer.Resolve<SecondViewModel>();
        _regionManager.Regions["MainRegion"].Add(view);
        _regionManager.Regions["MainRegion"].Activate(view);
    }
    #endregion
}
Ничего сложного в данной модели представления для вас не должно быть. Здесь, по сути, только одна команда ButtonCommand, которая реализует метод ShowSecondViewдля того чтобы с помощью view injection заменить в нашем регионе представление на другое. И реализация SecondViewModel выглядит очень похоже.
public class SecondViewModel : BindableBase
{
    #region Private Variables
    private readonly IRegionManager _regionManager;
    private readonly IUnityContainer _unityContainer;
    #endregion

    #region Constructor
    public SecondViewModel(IRegionManager regionMananger, IUnityContainer container)
    {
        _regionManager = regionMananger;
        _unityContainer = container;
        ButtonCommand = new DelegateCommand(ShowFirstView);
    }
    #endregion

    #region Commands
    public DelegateCommand ButtonCommand { get; private set; }

    #endregion

    #region Private Methods
    private void ShowFirstView()
    {
        var view = _unityContainer.Resolve<FirstView>();
        view.DataContext = _unityContainer.Resolve<FirstViewModel>();
        _regionManager.Regions["MainRegion"].Add(view);
        _regionManager.Regions["MainRegion"].Activate(view);
    }
    #endregion
}
У этих классов настолько подобная логика, что для таких целей можно вынести всю логику в базовый класс и просто в производных переопределить метод, который реализует view injection. После того как мы создали наши модели, нам нужно при старте нашего приложения для первого представления FirstView установить DataContext как FirstViewModel. Для этого переходим в наш класс MainModule, в котором осуществляется начальная привязка, и переписываем метод Initialize() вот так:
public void Initialize()
{
    var view = _container.Resolve<FirstView>();
    view.DataContext = _container.Resolve<FirstViewModel>();
    _regionManager.Regions["MainRegion"].Add(view);
}
Теперь осталась самая простая и самая легкая часть, а именно связать кнопки с соответствующими командами с моделей представлений. Первой под нашу правку попадает контрол FirstView. В нем ищем, где находятся кнопки, и для первой кнопки устанавливаем байндинг.
StackPanel Orientation="Horizontal">
    <Button Content="Button1" Margin="3,2" Command="{Binding ButtonCommand}"/>
    <Button Content="Button2" Margin="3,2"/>
</StackPanel>
Теперь переходим на контрол SecondView, ищем там кнопки и теперь уже для второй кнопки устанавливаем байндинг.
<StackPanel Orientation="Horizontal">
    <Button Content="Button1" Margin="3,2"/>
    <Button Content="Button2" Margin="3,2" Command="{Binding ButtonCommand}"/>
    <Button Content="Button3" Margin="3,2"/>
</StackPanel>
После этого вы можете запустить ваше приложение, и у вас должна заработать смена представлений.
Надеюсь, что это небольшое введение поможет вам использовать динамическую подгрузку представлений в ваши регионы, используя для этого view injection. Информацию для понимания, в какую сторону нужно копать глубже, я очень старался донести. 

Monday, November 24, 2014

Кёльн. Строгий город с готическим дыханием

Сегодня предлагаю погрузиться в захватывающий мир такой страны, как Германия. Мне не так давно посчастливилось провести там три интересных дня, посетив Кёльн, Франкфурт-на-Майне и Мюнхен. Каждый из этих городов по-своему привлекателен. Но начну свою историю путешествия, пожалуй, с города Кёльн. 
Одни из тех вещей, которыми славится Германия, и о которых вы, вероятно, неоднократно слышали, – это немецкие машины (BMWMercedes), знаменитое немецкое пиво и популярный национальный праздник – Октоберфест. Ну и как можно говорить о Германии, не упомянув о местных железных дорогах. В Германии просто шикарные железные дороги и не менее отличные поезда. Но поскольку обслуживание таких дорог стоит больших денег, то и билеты на поезда недешевые. Но у них есть очень много разных бонусных программ, по которым путешествие железными дорогами Германии будет не так сильно ударять по вашему бюджету. В Кёльн мы ехали с Брюсселя вот на таком красавце:
Как показала практика, лучше приобретать билеты с привязкой сразу к месту. Дело в том, что места в таких поездах только сидячие и делятся на места первого и второго класса. Немцы не проверяют у пассажиров билеты перед посадкой в поезд, но вас точно ждет контроль билета в самом поезде. Поэтому лучше сразу убедитесь, что у вас билет при себе, если не хотите быть высаженным посреди дороги и провести время совместно с немецкой полицией. В поездке на немецких дорогах вас приятно удивит отсутствие шумного стука колес и возможность просто наслаждаться тихой поездкой.
Город Кёльн встретил нас шумным, но одновременно просторным жд вокзалом, который, к сожалению, ничем примечательным не запомнился. Сооружения здесь сделаны в характерном для немцев классическом стиле.
В самом центре снять жилье для ночлега стоит огромных денег, поэтому в целях экономии придется  немного проехать на метро. При выходе с метро вы увидите перед собой просторную полупустую площадь с простыми фонтанами.
Справа видно мост через Рейн, а позади находится знаменитый Кёльнский собор. Фото собора с ракурса реки Рейн, а также со стороны железнодорожного вокзала приведу чуточку позже. Также выходя с вокзала, вы можете сразу увидеть посадку на метро, которая выглядит очень необычно.
Кёльн – очень большой город, и посмотреть его весь за день просто невозможно. Мы успели увидеть только маленькую часть этого города. Рекомендую вам как минимум пройтись к мосту, чтобы посмотреть на Кёльнский собор со стороны Рейна. Это выглядит захватывающе. Да что я буду об этом говорить, посмотрите на это сами.
А вот вид на Кёльнский собор с моста через Рейн. Ракурс немного неудачный, но, надеюсь, позволит хоть немного представить необыкновенную красоту и величие этой готической жемчужины.
Столб слева немного мешает обзору, но, к сожалению, более лучшего места для съемки мы не смогли найти. С моста открывается пленительный вид на город и на реку.
Немного предыстории о самом мосте. Этот железнодорожный мост, возле которого были сделаны эти замечательные кадры, называется Гогенцоллерн. Очень сложнопроизносимое название. Но он имеет более просто название, а именно: "мост любви". Дело в том, что этот мост усеян тысячами самых разнообразных замков и замочков, которые вешали влюбленные, выбрасывая ключи от них в Рейн.  Когда мы его пересекали, то видели, как несколько пар вешали на этот мост новые замочки.
А теперь представьте себе количество этих замков. Дело в том, что мост обвешан замками с двух сторон, при том что длина его в основном пролете составляет 167,75 м, а полная длина моста – 409,19 м. Там были настолько неординарные замки, что мои коллеги по работе, которые путешествовали вместе со мной, сделали несколько фото, которыми хочу с вами поделиться.
Кодовый маленький замок:
А вот кодовый замок, более раритетный:
Замочек в виде сердца:
Ну и замок в виде птицы:
Что поражает в Кёльне, – так это чистота улиц. Практически везде стоят урны для мусора, и действительно редкость увидеть случайно брошенную обертку от конфеты, окурок или бутылку с пива. Правда, в районе моста влюбленных было все-таки грязновато. Множество страшных корявых граффити, неумело нарисованных, не придают особого колорита. Но такая ситуация наблюдалась нами только возле моста и сразу возле жд вокзала. Мне очень понравились указатели для велосипедистов, так что я решил это запечатлеть.
Дело в том, что до этого я побывал в Амстердаме, где на велосипедах ездят практически все, но таких указателей я там не видел. Сразу видно почерк немцев в том, что они делают. Также тот факт, который поразил нас еще больше, – это то, что билеты на проезд в трамвае или метро можно купить в самом транспорте. Но отличие в том, что вы покупаете билет не у кондуктора, как в некоторых странах, а в автомате, установленном в транспорте. Плохо, что мы так и не поняли, сколько же времени действует такой билет. Еще больше нас удивил разный вид этих билетов. Билет, купленный в кассе, имел одну форму, а в автоматах – совсем другую. В Кёльне много жителей с Турции, поэтому в этих автоматах один из языков, позволяющий выбрать автомат по продаже билетов на проезд, – это турецкий. Ну и метро найти не составляет особого труда в Кёльне. Главное – ориентироваться по буквам DB (deutsche bahn) – немецкие железные дороги, или "U", или "S".
У немцев очень дорогие сигареты. Поэтому курение здесь вам влетит в копеечку. Теперь еще один неприятный нюанс, который нас застал в одном из супермаркетов города Кёльн. В местных магазинах не принимают карточки Visa и Mastercard. Это было неприятно для нас, так как в основном покупки мы совершали по карточке, чтобы по минимуму тратить наличные, а вопросы конвертации валюты мы хотели возложить на свои банки. Поэтому при походе по местным супермаркетам запаситесь наличкой.
Теперь расскажу вам б еще одной особенности немцев в построении домов за городом. Практически все дома имеют острую крышу, в которой сделан второй этаж. Там обычно делают спальню или кухню. Вот пример таких домиков:
Еще один замечательный домик:
Пример загородной немецкой улочки:
Удивителен тот факт, что если посмотреть на фото, то видно, что дорога латалась неоднократно, но, к сожалению, на фото не покажешь тот факт, что дорога ровная. Обычно у нас латки на дорогах стараются объезжать десятой дорогой, так как это автоматически означает, что там либо горб, либо яма.
Вот фото еще одного замечательного дворика, усаженного деревьями.
Ну и как же побывать в Кёльне и не попробовать их знаменитого пива Кельш, сваренное местными пивоварами.
Признаться честно,  это пиво нас не очень впечатлило. Полагаю, мы были просто разбалованы бельгийским пивом, которое я считаю самым лучшим. А вот история, которая нас постигла с этим пивом, более интересная. Для начала небольшая предыстория с нашего визита в Германию. Многие немцы не знают английский язык; исключение, наверное, составляют работающие в IT-сфере или те, которым попросту приходится с этим работать. Мало того, немцы откровенно не любят английский и делают все для того, чтобы показать гостям, кто в доме хозяин. Так вот, мы, три уникума, которые по-немецки не знали вообще ничего, кроме "привет", "пока" и "спасибо", пошли в один немецкий ресторанчик, чтобы испробовать местную кухню. Минут пять мы ждали, пока освободится столик и решили сделать себе заказ. Первым делом немец, который нас обслуживал, поинтересовался, знаем ли мы немецкий. На что, соответственно, получил отрицательный ответ. После этого он нам принес меню и подсунул доску, на которой на немецком были написаны фирменные блюда дня, и сказал попробовать что-либо с этого понять. Он вернулся через несколько минут с подносом, на котором было выложено самое разное сырое мясо и рыба, и начал нам объяснять на английском, как это мясо называется по-немецки. Выглядело это нелепо, но одновременно мы хоть немного поняли, что будем есть. Блюда у них, конечно, очень вкусные, но порции намного меньше, чем те, к которым мы привыкли в Бельгии. Я себе заказал лосось с рисом.
Блюдо было очень аппетитным и нежным. А ребята заказали себе картофель фри с овощами 
и еще что-то, название чего мы, соответственно, не запомнили. 
Немецкой кухней мы остались довольны. А когда мы уходили, немец, который нас обслуживал, попрощался с нами за руку. Было в целом очень приятно.
Так как времени особо надолго оставаться в Кельне у нас не было,поскольку мы заранее спланировали маршрут и в планах было посетить три немецких города, на каждый из которых выделили один день. Прогулка была сложной: целый день пешком, затем дорога и следующий пункт в маршруте. Но мы были в целом готовы к этому, потому брали с собой только рюкзаки, чтобы можно было налегке гулять подольше.
Мы решили проснуться пораньше и сходить поближе к самому Кёльнскому собору, чтобы успеть на поезд и оставить силы еще на Франкфурт, а то приехать в город и ничего не посмотреть в наши планы не входило. 
Вот такая вот скульптура находится между зданием жд вокзала и Кельнского собора. Ну и сам Кёльнский собор вблизи, ради которого мы совершили столь долгий путь.
Еще несколько ракурсов этого собора:
Возле входа в сбор стоит вот такой интересный монумент:
Вид на собор со стороны главного входа показан ниже.
Ну и по дороге от собора к жд вокзалу, на который мы сразу отправились после сбора, чтобы купить магнитики на память об этом замечательном городе, мы успели сфотографировать еще пару немецких красавцев:  
На переднем фоне – знаменитый автомобиль Мерседес, а чуточку дальше стоит Мазератти. Чуть спереди еще стоял очень симпатичный зеленый Ламборджини, который мы сфотографировали, но я решил ее сюда не добавлять, так как это не достопримечательность Германии. 
На этом наше путешествие по этом замечательном городе закончилось, и мы быстро побежали за нашими магнитиками и сели на поезд, потому что впереди нас ожидал знаменитый город Франкфурт-на-Майне. Надеюсь, рассказ был не очень скучным и вы смогли окунуться хотя бы капельку в культуру и красоту Германии. Впереди нас ждет волшебные путешествие в города Франкфурт-на-Майне и Мюнхен, о которых поведаю в следующих статьях.