Search code examples
c#wpfmvvmmahapps.metrohamburger-menu

Set the data context of a custom control of mahapps hamburger menu tag


I have come across lots of questions regarding this topic, however, I haven't found one that would solve my problem. For example, I can't use this solution since I'm using MVVM and I register my view models with the help of the host builder. This question sets a view model's property to a text block's text property with the help of this one. I have been trying to use the Binding Proxy but couldn't get it to work.

To be more specific, I use MVVM and so I don't have default constructors. I set my view's data context in my App.xaml like this:

<Application.Resources>
     <ResourceDictionary>

           ..

          <DataTemplate DataType="{x:Type viewmodels:HomeViewModel}">
                <views:HomeView />
          </DataTemplate>

          <DataTemplate DataType="{x:Type viewmodels:LogoutViewModel}">
               <views:LogoutView />
          </DataTemplate>

     </ResourceDictionary>
</Application.Resources>

My user control on which I have the hamburger menu looks like this:

HamburgerMenuRipple.xaml

<mah:HamburgerMenu
        x:Name="HamburgerMenuControl"
        DisplayMode="CompactInline"
        HamburgerButtonClick="HamburgerMenuControl_HamburgerButtonClick"
        IsPaneOpen="True"
        ItemInvoked="HamburgerMenuControl_OnItemInvoked"
        ItemTemplate="{StaticResource MenuItemTemplate}"
        OpenPaneLength="275"
        OptionsItemTemplate="{StaticResource MenuItemTemplate}"
        SelectedIndex="0"
        Style="{StaticResource MahApps.Styles.HamburgerMenu.Ripple}"
        VerticalScrollBarOnLeftSide="False">

        <!--  Header  -->
        <mah:HamburgerMenu.HamburgerMenuHeaderTemplate>
            <DataTemplate>
                <TextBlock
                    Padding="10,0,0,0"
                    HorizontalAlignment="Left"
                    VerticalAlignment="Center"
                    FontSize="16"
                    FontWeight="DemiBold"
                    Foreground="White"
                    TextWrapping="Wrap" />
            </DataTemplate>
        </mah:HamburgerMenu.HamburgerMenuHeaderTemplate>


        <!--  Items  -->
        <mah:HamburgerMenu.ItemsSource>

            <mah:HamburgerMenuItemCollection>
                <mah:HamburgerMenuIconItem Icon="{iconPacks:Material Kind=Home}" Label="Home" />

                <mah:HamburgerMenuIconItem Icon="{iconPacks:Material Kind=Bell}" Label="Notifications" />

                ..

            </mah:HamburgerMenuItemCollection>
        </mah:HamburgerMenu.ItemsSource>

        <!--  Options  -->
        <mah:HamburgerMenu.OptionsItemsSource>
            <mah:HamburgerMenuItemCollection>
                <mah:HamburgerMenuSeparatorItem x:Name="Separator_2" IsVisible="{Binding ElementName=Separator_2}" />

                <mah:HamburgerMenuIconItem Icon="{iconPacks:Material Kind=Cog}" Label="Settings" />

                <mah:HamburgerMenuIconItem Icon="{iconPacks:Material Kind=Logout}" Label="Logout">
                    <mah:HamburgerMenuIconItem.Tag>
                        <views:LogoutView/> <!--How can I set the datacontext here?-->
                    </mah:HamburgerMenuIconItem.Tag>
                </mah:HamburgerMenuIconItem>

            </mah:HamburgerMenuItemCollection>
        </mah:HamburgerMenu.OptionsItemsSource>

        <mah:HamburgerMenu.ContentTemplate>
            <DataTemplate DataType="{x:Type mah:HamburgerMenuIconItem}">
                <Grid Margin="20,0,10,0">
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto" />
                        <RowDefinition Height="*" />
                    </Grid.RowDefinitions>
                    <TextBlock
                        Grid.Row="0"
                        Margin="0,15,0,5"
                        Padding="0"
                        FontFamily="{DynamicResource MahApps.Fonts.Family.Header}"
                        FontSize="16"
                        Text="{Binding Label}" />
                    <ScrollViewer
                        Grid.Row="1"
                        Focusable="False"
                        HorizontalScrollBarVisibility="Disabled"
                        VerticalScrollBarVisibility="Auto">
                        <ContentControl Content="{Binding Tag}" Focusable="False" />
                    </ScrollViewer>
                </Grid>
            </DataTemplate>
        </mah:HamburgerMenu.ContentTemplate>
    </mah:HamburgerMenu>

My Logout view is simple for now, but it looks like this:

LogoutView.xaml

<StackPanel Orientation="Vertical">
        <TextBlock Text="Do you want to log out?" />
        <Button Command="{Binding LogoutCommand}" Content="Log out" />
    </StackPanel>

And the view model:

LogoutViewModel.cs

    public class LogoutViewModel : ViewModelBase
        {
            public ICommand LogoutCommand { get; }
    
            public LogoutViewModel(IAuthenticationService authenticationService, INavigationService loginNavigationService)
            {
                LogoutCommand = new LogoutCommand(this, authenticationService, loginNavigationService);
            }
        }

AddViewModelsHostBuilderExtensions.cs

 public static IHostBuilder AddViewModels(this IHostBuilder host)
        {
            host.ConfigureServices(services =>
            {
                services.AddTransient<MainViewModel>();

                services.AddSingleton<CreateViewModel<LoginViewModel>>(services => () => CreateLoginViewModel(services));
                services.AddSingleton<CreateViewModel<HomeViewModel>>(services => () => CreateHomeViewModel(services));
                services.AddSingleton<CreateViewModel<LogoutViewModel>>(services => () => CreateLogoutViewModel(services));
            });

            return host;
        }

        private static LoginViewModel CreateLoginViewModel(IServiceProvider services)
        {
            return new LoginViewModel(
                services.GetRequiredService<NavigationService<HomeViewModel>>());
        }

        private static HomeViewModel CreateHomeViewModel(IServiceProvider services)
        {
            return new HomeViewModel();
        }

        private static LogoutViewModel CreateLogoutViewModel(IServiceProvider services)
        {
            return new LogoutViewModel(
                services.GetRequiredService<NavigationService<LoginViewModel>>());
        }

My view appears correctly when I click the 'Log out' item, however, whenever I click on the 'Log out' button, the command doesn't fire. Is it because the view doesn't have the view model's data context (according to the other questions)? Or did I make a mistake somewhere else? How do I set the data context of the view in this situation?


Solution

  • Fortunately, @mm8 's comment helped me to start working in the right direction, and then I could figure out the rest by myself, so I post my solution here, hope it might help others in the future.

    Basically, what I did was that I created a view model property in my HomeViewModel. I guess you can create it in this property, but I register view models in my host builder so I get it from my constructor.

    HomeViewModel.cs

    public class HomeViewModel : ViewModelBase
        {
            public LogoutViewModel LogoutViewModel { get; }
    
            public HomeViewModel(LogoutViewModel logoutViewModel)
            {
                LogoutViewModel = logoutViewModel;
            }
        }
    

    After this, I created a dependency property in my hamburger menu user control so that I can pass the view model from the HomeViewModel to the hamburger menu user control.

    HomeView.xaml

    ..
    <controls:HamburgerMenuRipple LogoutViewDataContext="{Binding LogoutViewModel}" />
    ..
    

    HamburgerMenuRipple.xaml.cs

    public static readonly DependencyProperty LogoutViewDataContextProperty =
                DependencyProperty.Register(nameof(LogoutViewDataContext), typeof(LogoutViewModel), typeof(HamburgerMenuRipple), new PropertyMetadata(null));
    
            public LogoutViewModel LogoutViewDataContext
            {
                get { return (LogoutViewModel)GetValue(LogoutViewDataContextProperty); }
                set { SetValue(LogoutViewDataContextProperty, value); }
            }
    

    And then I set the data context of the tag's user control with the help of the OnItemInvoked command.

    private void HamburgerMenuControl_OnItemInvoked(object sender, HamburgerMenuItemInvokedEventArgs e)
            {
                HamburgerMenuControl.Content = e.InvokedItem;
                var iconItemTag = (e.InvokedItem as HamburgerMenuIconItem).Tag;
    
                if (iconItemTag != null)
                {
                    switch(iconItemTag)
                    {
                        case LogoutView:
                            ErrorMessage = string.Empty;
                            (iconItemTag as UserControl).DataContext = LogoutViewDataContext;
                            break;
    
                        default:
                            ErrorMessage = "No data context has been bound to this view.";
                            break;
                    }
                }
            }
    

    HamburgerMenuRipple.xaml

    <mah:HamburgerMenu
                x:Name="HamburgerMenuControl"
                DisplayMode="CompactInline"
                IsPaneOpen="True"
                ItemInvoked="HamburgerMenuControl_OnItemInvoked"
    
    ..
    
    <mah:HamburgerMenu.OptionsItemsSource>
                    <mah:HamburgerMenuItemCollection>
                        <mah:HamburgerMenuSeparatorItem />
    
                        <mah:HamburgerMenuIconItem
                            x:Name="LogoutIconItem"
                            Icon="{iconPacks:Material Kind=Logout}"
                            Label="Logout">
                            <mah:HamburgerMenuIconItem.Tag>
                                <views:LogoutView HorizontalAlignment="Center" VerticalAlignment="Center" />
                            </mah:HamburgerMenuIconItem.Tag>
                        </mah:HamburgerMenuIconItem>
    
                    </mah:HamburgerMenuItemCollection>
                </mah:HamburgerMenu.OptionsItemsSource>
    
    ..