Home > OS >  WPF Navigation while Keeping Menubar (Header) and Footer Fixed
WPF Navigation while Keeping Menubar (Header) and Footer Fixed

Time:09-16

We used to develop application with WinForms and nowadays we are trying to migrate it to WPF, starting from zero. In our application we have 3 main parts on screen which are Header (all main menu items), body (based on MDI container, content can be changed) and the footer (where general status is displayed, logo etc.) Whenever a user clicks on different menuitem from header part, the body part would change it's children to that Panel/Form.

There are lot's of good examples/tutorials on the Internet but I am confused about how to achieve to create a navigation service that allows to switch the view of body part.

Any suggestions would be appriciated, thanks in advance.

CodePudding user response:

There are indeed multiple ways to archive this result. I will try and explain the very basic/easiest way to get the result. While this will not provide example in combination with Menu Control, I think it will help you to understand the concept

In your MainWindow you can split use Grid layout and split the space into 3 parts as you wanted. Your Main window Xaml should look something like this :

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="50"/>
        <RowDefinition Height="*"/>
        <RowDefinition Height="50"/>
    </Grid.RowDefinitions>
    <ContentControl x:Name="Header"/>
    <ContentControl x:Name="Content" Grid.Row="1/>
    <ContentControl x:Name="Footer" Grid.Row="2"/>
</Grid>

In your content control you can insert your "UserControls" for the Header,Content,Footer. Now to the navigation part: As mentioned there are many ways to archive this and I will describe what I do consider the easiest way (not the most flexible way however, so keep that in mind).

First I will suggest to make a navigation Model:

public class NavigationModel
{
    public NavigationModel(string title, string description, Brush color)
    {
        Title = title;
        Description = description;
        Color = color;
    }

    public string Title { get; set; }
    
    public string Description { get; set; }
    
    public Brush Color { get; set; }

    public override bool Equals(object obj)
    {
        return obj is NavigationModel model &&
               Title == model.Title &&
               Description == model.Description &&
               Color == model.Color;
    }

    public override int GetHashCode()
    {
        return HashCode.Combine(Title, Description, Color);
    }
}

We create a new class that will handle the navigation collection, lets call it navigation service.

    public class NavigationService
{

    public List<NavigationModel> NavigationOptions { get=>NavigationNameToUserControl.Keys.ToList(); }

    public UserControl NavigateToModel(NavigationModel _navigationModel)
    {
        if (_navigationModel is null) 
            //Or throw exception
            return null;
        if (NavigationNameToUserControl.ContainsKey(_navigationModel))
        {
            return NavigationNameToUserControl[_navigationModel].Invoke();
        }
        //Ideally you should throw here Custom Exception
        return null;
    }

    //Usage of the Func, provides each call new initialization of the view
    //If you need initialized views, just remove the Func
    //-------------------------------------------------------------------
    //Readonly is used only for performance reasons
    //Of course there is option to add the elements to the collection, if dynamic navigation mutation is needed
    private readonly Dictionary<NavigationModel, Func<UserControl>> NavigationNameToUserControl = new Dictionary<NavigationModel, Func<UserControl>>
    {
        { new NavigationModel("Navigate To A","This will navigate to the A View",Brushes.Aqua), ()=>{ return new View.ViewA(); } },
        { new NavigationModel("Navigate To B","This will navigate to the B View",Brushes.GreenYellow), ()=>{ return new View.ViewB(); } }
    };

    #region SingletonThreadSafe
    private static readonly object Instancelock = new object();
    
    private static NavigationService instance = null;

    public static NavigationService GetInstance
    {
        get
        {
            if (instance == null)
            {
                lock (Instancelock)
                {
                    if (instance == null)
                    {
                        instance = new NavigationService();
                    }
                }
            }
            return instance;
        }
    }
    #endregion
}

This service will provide us with action to receive desired UserControll (note that I am using UserControl instead of pages, since they provide more flexibility).
Not we create additional Converter, which we will bind into the xaml:

   public class NavigationConverter : MarkupExtension, IValueConverter
{
    private static NavigationConverter _converter = null;

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        if (_converter is null)
        {
            _converter = new NavigationConverter();
        }
        return _converter;
    }

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        NavigationModel navigateTo = (NavigationModel)value;
        NavigationService navigation = NavigationService.GetInstance; 
            if (navigateTo is null) 
            return null;
        return navigation.NavigateToModel(navigateTo);
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        => null;
}

In our MainWindows.xaml add reference to the Converter namespace over xmlns, for example :

xmlns:Converter="clr-namespace:SimpleNavigation.Converter"

and create insance of converter :

<Window.Resources>
    <Converter:NavigationConverter x:Key="NavigationConverter"/>
</Window.Resources>

Note that your project name will have different namespace And set the Add datacontext to the instance of our Navigation Service: You can do it over MainWindow.Xaml.CS or create a ViewModel if you are using MVVM

MainWindow.Xaml.CS:

 public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        DataContext = Service.NavigationService.GetInstance.NavigationOptions;
    }
}

Now all is left to do is navigate. I do not know how about your UX, so I will just provide example from my github of the MainWindow.xaml. Hope you will manage to make the best of it :

<Grid>
<Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto"/>
        <ColumnDefinition />
    </Grid.ColumnDefinitions>
    <StackPanel>
        <ListView 
            x:Name="NavigationList"
            ItemsSource="{Binding}">
        <ListView.ItemTemplate>
            <DataTemplate>
                <Border 
                    Height="35"
                    BorderBrush="Gray"
                    Background="{Binding Color}"
                    ToolTip="{Binding Description}"
                    BorderThickness="2">
                    <TextBlock 
                        VerticalAlignment="Center"
                        FontWeight="DemiBold"
                        Margin="10"
                        Text="{Binding Title}" />
                </Border>
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>
    </StackPanel>
    <ContentControl
        Grid.Column="1"
        Content="{Binding ElementName=NavigationList,Path=SelectedItem,Converter={StaticResource NavigationConverter}}"/>
</Grid>

Just in case I will leave you a link to github, so it will be easier for you https://github.com/6demon89/Tutorials/blob/master/SimpleNavigation/MainWindow.xaml

Using same principle to use Menu Navigation

  <Window.DataContext>
    <VM:MainViewModel/>
</Window.DataContext>
<Window.Resources>
    <Converter:NavigationConverter x:Key="NavigationConverter"/>
</Window.Resources>

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="*"/>
        <RowDefinition Height="50"/>
    </Grid.RowDefinitions>
    <Menu>
        <MenuItem Header="Navigaiton" 
                ItemsSource="{Binding NavigationOptions}">
                <MenuItem.ItemTemplate>
                    <DataTemplate>
                        <MenuItem 
                            Command="{Binding DataContext.NavigateCommand, RelativeSource={RelativeSource AncestorType=Window}}"
                            CommandParameter="{Binding}"
                            Header="{Binding Title}"
                            Background="{Binding Color}"
                            ToolTip="{Binding Description}">
                        </MenuItem>
                    </DataTemplate>
                </MenuItem.ItemTemplate>
        </MenuItem>
    </Menu>
    <ContentControl 
        Grid.Row="1"
        Background="Red"
        BorderBrush="Gray"
        BorderThickness="2"
        Content="{Binding CurrentView,Converter={StaticResource NavigationConverter}}"/>
    <Border Grid.Row="2" Background="{Binding CurrentView.Color}">
        <TextBlock Text="{Binding CurrentView.Description}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
    </Border>
</Grid>

And we have in VM List of the navigation Models, Current Model and the navigation command :

  public class MainViewModel:INotifyPropertyChanged
{
    public List<NavigationModel> NavigationOptions { get => NavigationService.GetInstance.NavigationOptions; }

    private NavigationModel currentView;

    public NavigationModel CurrentView
    {
        get { return currentView; }
        set 
        { 
            currentView = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("CurrentView"));
        }
    }

    RelayCommand _saveCommand;

    public event PropertyChangedEventHandler PropertyChanged;

    public ICommand NavigateCommand
    {
        get
        {
            if (_saveCommand == null)
            {
                _saveCommand = new RelayCommand(Navigate);
            }
            return _saveCommand;
        }
    }

    private void Navigate(object param)
    {
        if(param is NavigationModel nav)
        {
            CurrentView = nav;
        }
    }

}

Sorry for long reply

CodePudding user response:

I think that you not necessary have to start from scratch. You may have a look:

https://qube7.com/guides/navigation.html

  • Related