WPF XAML - wysuwane/chowane menu, przełaczanie widoków tylko z poziomu XAML

0

Uczę się właśnie WPF'a i chciałbym od razu wejść w architekturę MVVM.

(nie zajmuję się zawodowo programowaniem, to hobbystyczne zajęcie w którym czasami "popełniam" narzędzia do swojej obecnej pracy - inż. procesu w firmie produkcyjnej, dotychczas pisałem tylko w WinForms w którym bardziej lub mniej ale przeważnie używałem wzorca Model View Prezenter)

Teraz przy podejściu do programowania w WPF jestem na etapie przygotowania layoutu okna głównego i główne cele są takie:
1) cała logika ma być w XAMLu
2) z lewej strony ma być chowane lub wysuwane menu (chowanie i wysuwanie ma się odbywać poprzez naciśnięcie "różowego klawisza")
3) w oknie głównym mają się pojawiać odpowiednie widoki w zależności który klawisz z lewego menu się kliknie

Na razie ogarnąłem podstawy podstaw XAML'a i mam tak:

<DockPanel>
        <Grid Background="Blue" DockPanel.Dock="Top" Height="30">        </Grid>
        <Grid Background="Yellow" DockPanel.Dock="Bottom" Height="30">       </Grid>
        <StackPanel Background="Orange" DockPanel.Dock="Left" Width="150">
            <Button VerticalAlignment="Center" Margin="3" Height="40">
                <Grid Margin="2" >
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="30" ></ColumnDefinition>
                        <ColumnDefinition Width="110"></ColumnDefinition>
                    </Grid.ColumnDefinitions>
                    <Image HorizontalAlignment="Left" VerticalAlignment="Center" Source="/Grafiki/Ikonki/1.png" Margin="2,2,2,2"  Grid.Column="0"></Image>
                    <TextBlock TextWrapping="Wrap" TextAlignment="Left" VerticalAlignment="Center" Grid.Column="1">Opcja z długim tekstem 1</TextBlock>
                </Grid>
            </Button>
            <Button VerticalAlignment="Center" Margin="3" Height="40">
                <Grid Margin="2" >
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="30" ></ColumnDefinition>
                        <ColumnDefinition Width="110"></ColumnDefinition>
                    </Grid.ColumnDefinitions>
                    <Image HorizontalAlignment="Left" VerticalAlignment="Center" Source="/Grafiki/Ikonki/2.png" Margin="2,2,2,2"  Grid.Column="0"></Image>
                    <TextBlock TextWrapping="Wrap" TextAlignment="Left" VerticalAlignment="Center" Grid.Column="1">Opcja 2</TextBlock>
                </Grid>
            </Button>
            <Button VerticalAlignment="Center" Margin="3" Height="40">
                <Grid Margin="2" >
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="30" ></ColumnDefinition>
                        <ColumnDefinition Width="110"></ColumnDefinition>
                    </Grid.ColumnDefinitions>
                    <Image HorizontalAlignment="Left" VerticalAlignment="Center" Source="/Grafiki/Ikonki/3.png" Margin="2,2,2,2"  Grid.Column="0"></Image>
                    <TextBlock TextWrapping="Wrap" TextAlignment="Left" VerticalAlignment="Center" Grid.Column="1">Opcja 3</TextBlock>
                </Grid>
            </Button>
        </StackPanel>
        <Button DockPanel.Dock="Left" Background="pink" Width="10"></Button>
        <Grid Background="green">
            <TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" TextWrapping="Wrap">to jest okno główne które ma się rozsuwac po schowaniu menu z lewego StackPanelu, a nastepnie po wysunieciu lewego menu ma sie zwezic</TextBlock>
        </Grid>
    </DockPanel>

Wygląda to tak (kolory są dodane tylko żeby rozróżniać odpowiednie bloki)
ukladWPF.PNG

Pytania:
Ad2. w WinForms robiłem to poprzez umieszczenie klawisza z treścią ">>>" lub "<<<" w zależności czy menu było pokazane czy nie (oczywiście we właściwych UserControl'ach które dawały responsywne okno) ale w WinForms całą logikę tego menu miałem obsługiwaną zdarzeniami z poziomu kodu okna głównego tutaj chciałbym się dowiedzieć:
2.1. czy taka logika jest do zrobienia tylko w XAMLu?
2.2. a jeżeli tak to przynajmniej nakierujcie mnie jak to zrobić ewentualnie gdzie szukać sposobów na uzyskanie takiego zachowania?

Ad3 w WinForms takie pokazujące się widoki robiłem poprzez generowanie/wstawianie właściwych formatek UserControl w widoku okna głównego. Tutaj myślę o tym aby te różne widoki komponowąc na kolejnych Gridach ułożonych jeden na drugim a widoczność właściwemu widokowi dawać na podstawie ustawianej właściwości "Visible" zbindowanej z klawiszem z menu.
3.1. czy tak się robi?
3.2. jakie są inne sposoby na zrobienie takiej logiki tylko w XAML'u?

1

Zawsze logika kojarzyła mi się z Backendem, a to wtedy c# a nie XAML.
Ale wiem o co chodzi:

  1. DependencyProperty Zrób DP sygnalizujące, czy Twoje menu jest rozwinięte.
  2. Powiąż DoubleAnimation z tym DP rozwijając lub zwijając je w zależności od stanu.

jeżeli chodzi o widoczność kontrolek polecam stworzyć również DP z enum - public enum eCurrentModule. Potem za pomocą ValueConverter określać widoczność dla każdego "grida". Kiedyś zrobiłem sobie klasę do obsługi okienek. jest też w nim klasa konwertera bazująca na nazwie okna (u mnie to moduł)
https://pastebin.com/4eNGYUpF - może się przyda, ale nie bazuj na tym za bardzo, bo to moja głupkowata twórczość, co prawda działająca bardzo dobrze :)

1

Ad 2
Nie wiem czy na przycisku możną osiągnąć taki efekt w samym xamlu, ale na ToggleButton powinno się udać.
Korzystając z triggerów kontrolki, sprawdzasz na toggle buttonie właściwość IsChecked i według niej operoujesz contentem tej kontrolki.
Używając tej samej właściwości na pomarańczowej kontrolce korzystasz z animacji i odpowiednio zmniejszasz/zwiększasz szerokość menu według IsChecked na ToggleButton'ie

Ad 3
Można na visibility, ale to słabe podejście.
Ja robię tak, że w głównym viewmodel'u tworzę listę viewmodel'i, tychże widoków oraz właściwość aktualnie wybranego widoku. W xaml w resoursce głównego okna dodaję DataTemplate z właściwościa DataType ustawioną na każdy viewmodel widoku.W zawartości głównego okna wystarczy dodać ContentControl i zbindować się do właściwości aktualnie wybranego widoku.
Wtedy na każdy przycisk z menu ustawiasz odpowiedni viewmodel jako aktualnie wybrany i contentcontrol powinien podmienić datatemplate.

Mam nadzieję, że napisałem to w miarę zrozumiale. Jak coś to pytaj lub na pw to coś zdziałamy :)

0

@Piotr.Net: Dzięki, właśnie doczytałem, doszukałem i zrobiłem to chowające się menu, prośbę mam jeszcze o rozwiniecie tego systemu podmieniania zawartości okna glownego który opisałeś, jeżeli dobrze rozumiem wszystko wg Twojej metody będzie w XAMLu bez żadnego kodu C#?

<Window x:Class="XAML_testy_nauka.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:XAML_testy_nauka"
        xmlns:converter="clr-namespace:XAML_testy_nauka.WidokNarzedzia"
        mc:Ignorable="d"
        Title="MainWindow" Height="600" Width="800" MinHeight="600" MinWidth="800">
    <Window.Resources>
        <LinearGradientBrush x:Key="JasnoSzaryGradient" StartPoint="0,0.5" EndPoint="1,0.5">
            <GradientStop Color="DarkGray" Offset="0"/>
            <GradientStop Color="LightGray" Offset="1" />
        </LinearGradientBrush>
        <converter:BoolToggleBtnWidthMenuConverter x:Key="BoolToWidthConverter" />
    </Window.Resources>
    <DockPanel>
        <Grid x:Name="PanelNaglowka" Background="Blue" DockPanel.Dock="Top" Height="30">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="150"/>
                <ColumnDefinition Width="10"/>
                <ColumnDefinition />
                <ColumnDefinition Width="120" />
            </Grid.ColumnDefinitions>
            <ToggleButton x:Name="ToggleMenuBtn" Grid.Column="0" VerticalAlignment="Center"  Margin="2" Height="28">
                <ToggleButton.Style>
                    <Style TargetType="{x:Type ToggleButton}">
                        <Style.Triggers>
                            <Trigger Property="IsChecked" Value="False">
                                <Setter Property="Content" Value="Zwiń Menu"/>
                            </Trigger>
                            <Trigger Property="IsChecked" Value="True">
                                <Setter Property="Content" Value="Rozwiń Menu"/>
                            </Trigger>
                        </Style.Triggers>
                    </Style>
                </ToggleButton.Style>
            </ToggleButton>
            <Label x:Name="TytulOknaGlownego" VerticalAlignment="Center" Grid.Column="2">Tu ma być nagłówek otwartego/aktywnego okna</Label>
            <Label Grid.Column="3">Miejsce na logo</Label>
        </Grid>
        <Grid x:Name="PanelStopki" Background="Yellow" DockPanel.Dock="Bottom" Height="30"></Grid>
        <StackPanel x:Name="PanelMenu" Background="Orange" DockPanel.Dock="Left" Width="150" 
                    Visibility="{Binding ElementName=ToggleMenuBtn, Path=IsChecked, Converter={StaticResource BoolToWidthConverter}}">
            <Button VerticalAlignment="Center" Margin="3" Height="40" Foreground="GhostWhite" Background="{StaticResource JasnoSzaryGradient}">
                <Grid  Margin="2" >
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="30" ></ColumnDefinition>
                        <ColumnDefinition Width="110"></ColumnDefinition>
                    </Grid.ColumnDefinitions>
                    <Image HorizontalAlignment="Left" VerticalAlignment="Center" Source="/Grafiki/Ikonki/1.png" Margin="2,2,2,2"  Grid.Column="0"></Image>
                    <TextBlock TextWrapping="Wrap" TextAlignment="Left" VerticalAlignment="Center" Grid.Column="1">Opcja z długim tekstem 1</TextBlock>
                </Grid>
            </Button>
            <Button VerticalAlignment="Center" Margin="3" Height="40" Foreground="GhostWhite" Background="{StaticResource JasnoSzaryGradient}">
                <Grid Margin="2" >
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="30" ></ColumnDefinition>
                        <ColumnDefinition Width="110"></ColumnDefinition>
                    </Grid.ColumnDefinitions>
                    <Image HorizontalAlignment="Left" VerticalAlignment="Center" Source="/Grafiki/Ikonki/2.png" Margin="2,2,2,2"  Grid.Column="0"></Image>
                    <TextBlock TextWrapping="Wrap" TextAlignment="Left" VerticalAlignment="Center" Grid.Column="1">Opcja 2</TextBlock>
                </Grid>
            </Button>
            <Button VerticalAlignment="Center" Margin="3" Height="40" Foreground="GhostWhite" Background="{StaticResource JasnoSzaryGradient}">
                <Grid Margin="2" >
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="30" ></ColumnDefinition>
                        <ColumnDefinition Width="110"></ColumnDefinition>
                    </Grid.ColumnDefinitions>
                    <Image HorizontalAlignment="Left" VerticalAlignment="Center" Source="/Grafiki/Ikonki/3.png" Margin="2,2,2,2"  Grid.Column="0"></Image>
                    <TextBlock TextWrapping="Wrap" TextAlignment="Left" VerticalAlignment="Center" Grid.Column="1">Opcja 3</TextBlock>
                </Grid>
            </Button>
        </StackPanel>
        <Grid x:Name="OknoGlowne" Background="green">
            <TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" TextWrapping="Wrap">to jest okno główne które ma się rozsuwac po schowaniu menu z lewego StackPanelu, a nastepnie po wysunieciu lewego menu ma sie zwezic</TextBlock>
        </Grid>
    </DockPanel>
</Window>
1

W menu, o którym pisałem potrzebny jest kod c#. Przykładowo tak wygląda to o co mi chodziło:
C#

public class MainWindowViewModel : ViewModelBase
    {
        private ManuItemBase _selectedMenu;

        public MainWindowViewModel()
        {
            MenuItems = new List<ManuItemBase>()
            {
                new FirstViewModel(),
                new SecondViewModel(),
                new ThirdViewModel(),
            };
            SelectedMenu = MenuItems.FirstOrDefault();
            FirstMenuCommand = new RelayCommand<Type>(OnClickMenu);
        }

        private IEnumerable<ManuItemBase> MenuItems { get; set; }
        public ManuItemBase SelectedMenu { get => _selectedMenu; set => Set(ref _selectedMenu, value); }
        public ICommand FirstMenuCommand { get; set; }
        private void OnClickMenu(Type obj)
        {
            SelectedMenu = MenuItems.First(x => x.GetType().Name == obj.Name);
        }
    }

    public abstract class ManuItemBase 
    {
        public virtual string Title { get; }
    }

    public class FirstViewModel : ManuItemBase
    {
        public override string Title { get => "FirstMenu"; }
    }

    public class SecondViewModel : ManuItemBase
    {
        public override string Title { get => "SecondMenu"; }
    }

    public class ThirdViewModel : ManuItemBase
    {
        public override string Title { get => "ThirdMenu"; }
    }

XAML

<Window x:Class="WpfApp1.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:WpfApp1"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.Resources>
        <DataTemplate DataType="{x:Type local:FirstViewModel}">
            <TextBlock Text="{Binding Title}" Foreground="Red"></TextBlock>
        </DataTemplate>
        
        <DataTemplate DataType="{x:Type local:SecondViewModel}">
            <TextBlock Text="{Binding Title}" Foreground="Green"></TextBlock>
        </DataTemplate>
        
        <DataTemplate DataType="{x:Type local:ThirdViewModel}">
            <TextBlock Text="{Binding Title}" Foreground="Blue"></TextBlock>
        </DataTemplate>
    </Window.Resources>
    <Window.DataContext>
        <local:MainWindowViewModel></local:MainWindowViewModel>
    </Window.DataContext>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="1*"></ColumnDefinition>
            <ColumnDefinition Width="8*"></ColumnDefinition>
        </Grid.ColumnDefinitions>

        <StackPanel>
            <Button Command="{Binding FirstMenuCommand}" CommandParameter="{x:Type local:FirstViewModel}" Content="FirstView"></Button>
            <Button Command="{Binding FirstMenuCommand}" CommandParameter="{x:Type local:SecondViewModel}" Content="SecondView"></Button>
            <Button Command="{Binding FirstMenuCommand}" CommandParameter="{x:Type local:ThirdViewModel}" Content="ThirdView"></Button>
        </StackPanel>

        <ContentControl Grid.Column="1" Content="{Binding SelectedMenu}"></ContentControl>
    </Grid>
</Window>
1

jeżeli robisz tak:

MenuItems = new List<ManuItemBase>()
            {
                new FirstViewModel(),
                new SecondViewModel(),
                new ThirdViewModel(),
            };

to
public ManuItemBase SelectedMenu { get => _selectedMenu; set => Set(ref _selectedMenu, value); }
powinno być DependencyProperty albo chociaż wdróż do klasy INotifyPropertyChanged i dodaj po zmianie wartości PropertyChanged.

-> Powiadom kontrolki o tym, że zmieniasz ViewModel

1

DependencyProperty nie tworzy się w viewmodelu.
A powiadomienie jest w metodzie

Set<T>(ref T field, T newValue)

z klasy ViewModelBase.
Korzystam tutaj z MvvmLight. Ale autor uczy się mvvm, więc albo wie o co chodzi z tym Set albo wie o powiadamianiu zmiany właściwości, ewentualnie dopyta tutaj :)

1

@Piotr.Net: Dzięki za podpowiedzi, zrobiłem trochę na piechotę bo jednak nie wiem o co chodzi z tym Set'em [chyba jeszcze nie mój level ;)], jak możesz podpowiedz coś więcej co to jest i jak tego używać,

Set<T>(ref T field, T newValue)

To co mi zadziałało uzyskałem definiując INotifyPropertyChange w MainWindowViewModel

public class MainWindowViewModel : ViewModelBase, INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        private void OnPropertyChanged(params string[] nazwy)
        {
            if (PropertyChanged!=null)
            {
                foreach (var nazwa in nazwy)
                {
                    PropertyChanged(this, new PropertyChangedEventArgs(nazwa));
                }
            }
        }
 .............
}

i definiując klase RelayCommand jak nizej

internal class RelayCommand<T> : ICommand
    {
        private Action<Type> onClickMenu;

        public RelayCommand(Action<Type> onClickMenu)
        {
            this.onClickMenu = onClickMenu;
        }

        public event EventHandler CanExecuteChanged;

        public bool CanExecute(object parameter)
        {
            return true;
            //throw new NotImplementedException();
        }

        public void Execute(object parameter)
        {
            onClickMenu((Type)parameter);
            //throw new NotImplementedException();
        }
    }

i oczywiście jeszcze w MainWindowViewModel zmieniłem

public ManuItemBase SelectedMenu { get => _selectedMenu; set => Set(ref _selectedMenu, value); }

na klasyczną właściwość:

public ManuItemBase SelectedMenu
        {
            get
            {
                return _selectedMenu;
            }
            set
            {
                _selectedMenu = value;
                OnPropertyChanged("SelectedMenu");
            }
        }        
2

Klasa ViewModelBase, po której dziedziczyłem pochodzi z biblioteki MvvmLight. Jest to klasa, która implementuje INotifyProperyChanged oraz posiada metode Set<T>(ref T field, T newValue) , która robi mniej więcej to co Twoje OnPropertyChanged. Biblioteka również posiada implementacje ICommand.
Wybacz, po prostu z czasem nie zwraca się uwagi na takie wspólne elementy w MVVM i używa gotowców. Zwłaszcza jak chciałem Ci wyjaśnić co miałem na myśli :)
Dobrze jednak, że sam na początku implementujesz takie rzeczy

1 użytkowników online, w tym zalogowanych: 0, gości: 1