1

Basically what my problem is: I'm trying to display a ContentDialog in my WinUI 3 App.

The command to open the Dialog is triggered by a ContextMenu Button on my "Main Page" (LibraryPage) The content of the Dialog are a bunch of TextBoxes, which i define in another xaml (LibraryAddSongControl)

But I can't directly open the Dialog from my ViewModel because i cannot access the needed XamlRoot from there. And i cannot access the Dialog TextBoxes contents because those lie on the LibraryAddSongViewModel

LibraryPage.xaml
<controls:ListDetailsView
            x:Uid="Library"
            x:Name="ListDetailsViewControl"
            BackButtonBehavior="Manual"
            Background="Transparent"
            DetailsTemplate="{StaticResource DetailsTemplate}"
            ListHeader="Songs"
            ItemsSource="{x:Bind ViewModel.AllSongs}"
            ItemTemplate="{StaticResource ItemTemplate}"
            ListHeaderTemplate="{StaticResource MinimalListHeaderTemplate}"
            NoSelectionContentTemplate="{StaticResource NoSelectionContentTemplate}"
            SelectedItem="{x:Bind ViewModel.Selected, Mode=TwoWay}"
            ViewStateChanged="OnViewStateChanged">
            <controls:ListDetailsView.ContextFlyout>
                <MenuFlyout>
                    <MenuFlyoutItem Text="Add new Song" Icon="Add" Command="{x:Bind ViewModel.AddNewSongCommand}" />
                    <MenuFlyoutItem Text="Library Statistics" Icon="Help"
                                    Command="{x:Bind ViewModel.ShowLibraryStatisticsCommand}" />
                    <MenuFlyoutSeparator />
                    <MenuFlyoutItem Text="Delete Selected" Icon="Delete" Command="{x:Bind ViewModel.DeleteSongCommand}" />
                </MenuFlyout>
            </controls:ListDetailsView.ContextFlyout>
        </controls:ListDetailsView>
LibraryAddSongControl.xaml
<UserControl
    x:Class="Maestro.Views.Dialogs.LibraryAddSongControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:Maestro.Views.Dialogs"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:extensions="using:CommunityToolkit.WinUI.UI"
    mc:Ignorable="d">

    <Grid>
        <StackPanel>
            <TextBox Header="Titel" Text="{x:Bind ViewModel.Title, Mode=TwoWay}"/>
            <TextBox Header="Beschreibung" Text="{x:Bind ViewModel.Description, Mode=TwoWay}"/>
            <TextBox Header="Album" Text="{x:Bind ViewModel.Album, Mode=TwoWay}"/>
            <TextBox Header="Artist(s)" Text="{x:Bind ViewModel.Artists, Mode=TwoWay}"/>
            <TextBox Header="Dauer" Text="{x:Bind ViewModel.Duration, Mode=TwoWay}" extensions:TextBoxExtensions.Mask="99:99" />
            <TextBox Header="URL" Text="{x:Bind ViewModel.Url, Mode=TwoWay}"/>
            <!--<TextBox Header="Titel" Text="{x:Bind ViewModel.ThumbnailPath, Mode=TwoWay}"/>-->
        </StackPanel>
    </Grid>
</UserControl>

The ContextMenu command to open the ContentDialog from the LibraryViewModel

[RelayCommand]
    public async Task AddNewSong()
    {
        ContentDialog dialog = new();
        dialog.XamlRoot = //Not accessible from ViewModel!
        dialog.Style = Application.Current.Resources["DefaultContentDialogStyle"] as Style;
        dialog.Title = "Add a new Song to the Library";
        dialog.PrimaryButtonText = "Add";
        dialog.CloseButtonText = "Cancel";
        dialog.IsSecondaryButtonEnabled = false;
        dialog.DefaultButton = ContentDialogButton.Primary;
        dialog.Content = App.GetService<LibraryAddSongControl>();

        ContentDialogResult result = await dialog.ShowAsync();

        if (result == ContentDialogResult.Primary)
        {
            //Logic to handle Save command from Dialog
        }

    }
Logic in the LibraryAddSongViewModel that gets the contents of the Textboxes and saves them in an sqlite DB
public void AddSong()
    {

        SqliteHelper.AddSong(new Song
        {
            SongTitle = Title,
            SongDescription = Description,
            SongAlbum = Album,
            SongArtists = [$"{Artists}"],
            SongDuration = (60 * Convert.ToInt32(Duration.Substring(0, 2)) + Convert.ToInt32(Duration.Substring(3, 2))),
            SongURL = Url,
        });
    }

I tried to call the AddSong method of LibraryAddSongViewModel from the LibraryViewModel, but that resulted in the ViewModel properties that are bound to the Tedtbox contents to be empty, and not getting updated whenever i type something in the textbox

My ultimate question is, what is a clean MVVM-style solution to displaying a dialog from Xaml1, when the dialog itself also has Elements like Textboxes that have values which need to be processed (in my case, just saving them to a database)

1
  • Can't you access for example ListDetailsViewControl.XamlRoot? Commented Apr 15, 2024 at 6:55

3 Answers 3

1

I would try to avoid UI related code like XamlRoot in the ViewModel. Let me show you an example:

public class Item
{
    public string Name { get; set; }
}

public partial class MainViewModel : ObservableObject
{
    [ObservableProperty]
    private ObservableCollection<Item> _items =
    [
        new Item { Name = "Item 1" },
        new Item { Name = "Item 2" },
        new Item { Name = "Item 3" },
    ];

    [RelayCommand]
    private void AddNewItem(Item item) => Items.Add(item);
}

public sealed partial class MainPage : Page
{
    public MainPage()
    {
        this.InitializeComponent();
    }

    private MainViewModel ViewModel { get; } = new();

    private async void AddNewItemMenuFlyoutItem_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
    {
        var contentDialog = new ContentDialog
        {
            Title = "Add New Item",
            Content = new TextBox { PlaceholderText = "Enter item name" },
            PrimaryButtonText = "Add",
            CloseButtonText = "Cancel",
            XamlRoot = XamlRoot,
        };

        if (await contentDialog.ShowAsync() is not ContentDialogResult.Primary ||
            (contentDialog.Content as TextBox)?.Text is not string itemName ||
            itemName.Length is 0)
        {
            return;
        }

        var newItem = new Item { Name = itemName };
        ViewModel.AddNewItemCommand.Execute(newItem);
    }
}
<Page
    x:Class="App2.MainPage"
    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:local="using:App2"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
    mc:Ignorable="d">

    <Grid>
        <ListView ItemsSource="{x:Bind ViewModel.Items, Mode=OneWay}">
            <ListView.ItemTemplate>
                <DataTemplate x:DataType="local:Item">
                    <TextBlock Text="{x:Bind Name, Mode=OneWay}" />
                </DataTemplate>
            </ListView.ItemTemplate>
            <ListView.ContextFlyout>
                <MenuFlyout>
                    <MenuFlyoutItem
                        Click="AddNewItemMenuFlyoutItem_Click"
                        Text="Add new item" />
                </MenuFlyout>
            </ListView.ContextFlyout>
        </ListView>
    </Grid>

</Page>
Sign up to request clarification or add additional context in comments.

2 Comments

If we do it MVVM, then we try to avoid Click method in the Code Behind class. Like this post, where it tried to assign xaml in the constructor of the Code behind
@MihaiSocaciu MVVM is not about avoiding code behind, it's about separating business logic from UI logic. If a button interaction does something still on UI (displaying a dialog), then it belongs in the code behind. ViewModels should stay framework independent.
1
public partial class App : Application {
public static WindowEx MainWindow { get; } = new MainWindow(); }


[RelayCommand]
public async Task AddNewSong()
{
    ContentDialog dialog = new();
    dialog.XamlRoot = App.MainWindow.Content.XamlRoot;
}

Comments

0

You can also expose the root of your MainWindow as a static property so you can access it everywhere and display all dialogs centered.

Window.Content as FrameworkElement

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.