WPF: Handling Modal Dialogs - wpf

I found a nice solution on this webiste http://www.thesilvermethod.com/Default.aspx?Id=ModalDialogManagerAsimpleapproachtodealingwithmodaldialogsinMVVM
But had to do some changes to get it integrated into my code. Along the way I get some small problems mostly because there are certain parts of the code I'm not getting completely.
How I did it was to bind the ModalDialogManager to a MainWindow property of the Type IDialogViewModel. I then have a WindowsManager class that handles putting the right instance inside this property. One such is EditDialogViewModel that exposes a EditableViewModel to this DialogManager. I set the EditDialog view as a DataTemplate for this EditDialogViewModel but when I show it the new window only shows a part of it.
Here is the View:
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="EditDataTemplates.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="7*" />
<RowDefinition Height="2*" />
<RowDefinition Height="1*" />
</Grid.RowDefinitions>
<ContentControl Content="{Binding Path=ViewModel}" />
<TextBlock Text="{Binding Path=ViewModel.Error}" />
<UniformGrid Grid.Row="2" Columns="2">
<Button Command="{Binding SaveCommand}" />
<Button Command="{Binding CancelCommand}" />
</UniformGrid>
</Grid>
</UserControl>
But the new Dialog window only shows the ContentControl bound to the ViewModel property of EditDialogViewModel (it holds the ViewModel being edited).
My guess is it has something to do with this code in the ModelDialogManager:
void Show()
{
if (_window != null) Close();
Window w = new Window();
_window = w;
w.Closing += w_Closing;
w.Owner = GetParentWindow(this);
w.DataContext = this.DataContext;
w.SetBinding(Window.ContentProperty, ""); //This code here does something I don't fully understand
w.Title = Title;
w.Icon = Icon;
w.Height = DialogHeight;
w.Width = DialogWidth;
w.ResizeMode = DialogResizeMode;
w.ShowDialog();
}
He is applying the binding there but I guess it's only the first ContentControl that gets bound or something. It's quite tricky.
Another problem is that the mouse just doesn't work inside the Modal Dialog. I can tab into the textboxes but not click into them.
Is there a way to fix this or a better method to handle Modal Dialog boxes in WPF?
EDIT
Ok I'm going to admit it. I'm a huge idiot. This was so simple I just couldn't see it. I had set Height and Width on the UserControl to a fixed value while I was still messing around with it being a Window. So in actuality it was showing the whole thing, there just wasn't room. I have no idea why the mouse didn't work at that point but now it works perfectly.

Answering "a better method to handle Modal Dialog boxes in WPF?" there is a new control called Child Window in the WPF Extended Toolkit that addresses your Modal Dialog pains.

Related

WPF Caliburn Micro: Exchanging UserControls in a Window dynamically using ContentControl

This question is related to Add a usercontrol to caliburm micro dynamically.
I have read any other related threads before open this new thread, but I still don't understand and find no solution. Please accept my apology if some of you take this as duplicate.
I have a window (MainView) contains "main" Grid (aka LayoutRoot) with 2 columns.
On left column there are 2 buttons: "Display View 1" and "Display View 2".
If user click "Display View 1", the "Display1View" (is a UserControl contains TextBlock with Text "View 1") should be shown on the right column, replace the current one.
If user click "Display View 2", the "Display2View" (is a UserControl contains TextBlock with Text "View 2") should be shown on the right column, replace the current one.
My sample code contains following views and viewmodels:
MainView.xaml and MainViewModel.cs
Display1View.xaml and Display1ViewModel.cs
Display2View.xaml and Display2ViewModel.cs
In my sample code the ContentControl doesn't recognize the UserControl. What am I doing wrong? How to bind ContentControl correctly? Please feel free to modify my sample code. Thank you in advance
MainView.xaml
<Window x:Class="TestCaliMiContentControl.MainView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Main View"
Width="525"
Height="350">
<Grid x:Name="LayoutRoot" ShowGridLines="True">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="30*" />
<ColumnDefinition Width="100*" />
</Grid.ColumnDefinitions>
<StackPanel x:Name="LeftNavPanel" Grid.Column="0">
<Button x:Name="Display1" Content="Display View 1" />
<Button x:Name="Display2" Content="Display View 2" />
</StackPanel>
<ContentControl x:Name="MainGridContent" Grid.Column="1" />
</Grid>
</Window>
MainViewModel.cs
public class MainViewModel : PropertyChangedBase
{
private ContentControl _mainGridContent;
public ContentControl MainGridContent
{
get { return _mainGridContent; }
set
{
_mainGridContent = value;
NotifyOfPropertyChange(() => MainGridContent);
}
}
public void Display1()
{
//MainGridContent = new Display1ViewModel(); // cannot convert source type error
}
public void Display2()
{
// MainGridContent = new Display2ViewModel(); // cannot convert source type error
}
}
Display1View.xaml
<UserControl x:Class="TestCaliMiContentControl.Display1View"
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"
d:DesignHeight="300"
d:DesignWidth="300"
mc:Ignorable="d">
<Grid>
<TextBlock HorizontalAlignment="Center" FontSize="72"
Text="View 1"/>
</Grid>
</UserControl>
Display1ViewModel.cs
using System;
using System.Windows.Controls;
using Caliburn.Micro;
namespace TestCaliMiContentControl
{
public class Display1ViewModel : PropertyChangedBase {}
}
First, I would start by recommending you read the Caliburn.Micro documentation, specifically the part about Screens, Conductors, and Composition: http://caliburnmicro.com/documentation/composition
That being said, we can modify your code to get it working.
1) Since your MainViewModel is supposed to be conducting other items, it should descend from Conductor<T>. In this case, we will have it conduct the Caliburn Screen class.
public class MainViewModel : Conductor<Screen>
2) In MVVM, you view models should know nothing of your view. You should not see UI classes such as ContentControl. We could change your property to be of type Screen, but we actually don't need that property at all since we are using a conductor. So, remove the MainGridContent property and backing field.
3) Within your Display1 and Display2 methods, invoke Caliburn's conductor method ActivateItem to show the appropriate item.
public void Display1()
{
ActivateItem(new Display1ViewModel());
}
4) In your MainView.xaml you will need to bind your ContentControl to the conductor's active item property, which is, by convention, ActiveItem.
<ContentControl x:Name="ActiveItem" Grid.Column="1" />
5) Finally, since your conductor is conducting Screens, you need to make them screens. Screens are helpful because they have lifecycle and allow you to know when they are activated/deactivated. Do this for both Display1 and Display2.
public class Display1ViewModel : Screen {}
This should get you up and running.

AvalonDock Now Loses Alt Key Adornments

I've been using AvalonDock (2.0) for some time now, being key for managing documents in an IDE. In the last month or so, I've noticed that Alt key adornments are no longer showing up for controls within AvalonDock, though the Alt key commands are executing as expected. See the image below where the Alt key adornments are showing up in the menu, but not for the buttons inside AvalonDock:
What is particularly interesting about this issue, that it appears to be triggered by an environmental setting or condition.
As demonstrated in this video (at 2:07), the alt key adornments are working for a control within AvalonDock. But, if I now use the very same executable I used in that video, the alt key adornments do not work.
I'm currently using build 2.0.1746, but I also tried build 2.0.2000 (with the Xceed namespace) and found the same issue. I also tried the version packaged with the Xceed Extended WPF Toolkit, and found that the issue persists.
I also built a very simple test application which loads a couple of documents of type Item, where Item is a simple class with a Name property:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication1"
xmlns:avalonDock="clr-namespace:Xceed.Wpf.AvalonDock;assembly=Xceed.Wpf.AvalonDock"
xmlns:avalonDockLayout="clr-namespace:Xceed.Wpf.AvalonDock.Layout;assembly=Xceed.Wpf.AvalonDock"
xmlns:avalonDockControls="clr-namespace:Xceed.Wpf.AvalonDock.Controls;assembly=Xceed.Wpf.AvalonDock"
Title="MainWindow" Height="500" Width="500">
<Window.Resources>
<ResourceDictionary>
<DataTemplate DataType="{x:Type local:Item}">
<StackPanel Orientation="Vertical">
<Label Content="Item:" />
<TextBox Text="{Binding Name}"/>
<Button Content="_ClickMe" />
</StackPanel>
</DataTemplate>
</ResourceDictionary>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<avalonDock:DockingManager Grid.Row="0" DocumentsSource="{Binding}">
<avalonDockLayout:LayoutRoot>
<avalonDockLayout:LayoutPanel Orientation="Horizontal">
<avalonDockLayout:LayoutDocumentPane/>
</avalonDockLayout:LayoutPanel>
</avalonDockLayout:LayoutRoot>
</avalonDock:DockingManager>
<Button Grid.Row="1" Content="_Test" />
</Grid>
</Window>
Even with this simple application, the Alt key adornments show up for the Test button outside of AvalonDock, but not for the ClickMe buttons for the controls within AvalonDock.
I also posted the issue here on the AvalonDock codeplex site, but there appears to be very little response activity. I also posted the issue here on the Extended Toolkit codeplex site.
Any ideas on how to correct or workaround this issue?
It sounds as though this will be fixed with the next version of AvalonDock.
In the meantime, the following Blend behavior is a workaround:
public class FixKeyboardCuesBehavior : Behavior<UIElement>
{
private static readonly DependencyProperty ShowKeyboardCuesProperty;
static FixKeyboardCuesBehavior()
{
Type keyboardNavigation = typeof(KeyboardNavigation);
var field = keyboardNavigation.GetField("ShowKeyboardCuesProperty", BindingFlags.NonPublic | BindingFlags.Static);
Debug.Assert(field != null, "field != null");
ShowKeyboardCuesProperty = (DependencyProperty)field.GetValue(null);
}
protected override void OnAttached()
{
base.OnAttached();
Window rootWindow = Window.GetWindow(this.AssociatedObject);
if (rootWindow == null)
{
return;
}
BindingOperations.SetBinding(
this.AssociatedObject,
ShowKeyboardCuesProperty,
new Binding("(KeyboardNavigation.ShowKeyboardCues)") { Source = rootWindow });
}
}
Use this from XAML by adding the following to the root element of your DataTemplate for the AvalonDock LayoutItemTemplate:
<i:Interaction.Behaviors
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity">
<my:FixKeyboardCuesBehavior />
</i:Interaction.Behaviors>
This workaround uses internal implementation details of WPF to re-introduce the Alt behaviour below the broken AvalonDock logical tree. So, I'll be looking forward to being able to zap it from my code when AD itself is fixed!

Zooming in specific part of screen in WPF

I am implementing one small WPF application which has multiple rows and multiple columns. 0th row and 0th column contains a MediaElement and 1st row and 0th column contains a full screen button. When user clicks on full screen button I want to switch to a gird which has only two rows and one column. 0th row and 0th column will occupy most of the screen space having inside MediaElement and 1st row and 0th column will show a minimize button which will bring original UI back.
In traditional windows we were used to toggle visibility of a full screen panel hosting WindowsMedia player to achieve this behavior. How can I achieve this in WPF?
Adding my XAML code.
<Window x:Class="LearnEnglish.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow"
Height="350"
Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="5*" />
<RowDefinition Height="1*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="5*" />
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<MediaElement LoadedBehavior="Manual"
Name="me"
Source="C:\Users\Pritam\Documents\Freecorder\Screen\Northern Ireland Scene 1 LearnEnglish British Council.wmv"
Volume="{Binding ElementName=txtVolume,Path=Text}"
Grid.ColumnSpan="2">
</MediaElement>
<Button Click="Button_Click"
Grid.Row="1"
Margin="4">Play</Button>
<Button Click="Button_Click"
Grid.Row="1"
Grid.Column="1"
Margin="4">Full Screen</Button>
<Button Click="Button_Click"
Grid.Row="1"
Grid.Column="1"
Margin="4"
Visibility="Hidden">Restore</Button>
</Grid>
</Window>
When user clicks on 'Full Screen' button I want my 'MediaElement' to occupy most of the scree space ( by hiding all other controls ) and leaving 'Restore' button in the bottom-right hand side of screen.
Regards,
Hemant
You can do that completely in XAML by using a ToggleButton and a trigger on its IsChecked property which sets the width / height of all columns / rows you don't want to see to 0. Use x:Name to name the elements you want to change, that will make it easier to write the Trigger.
In order to be able to access all controls, you should define the trigger on a parent control which contains all the other controls, e.g. in a UserControl, a panel, a DataTemplate or ControlTemplate. In order to access the properties on different controls, use their names for the TargetName property on the setters. There is also a corresponding SourceName property on Trigger itself, so you don't have to define the Trigger on the ToggleButton itself.
Most simply, you would use code to change the size of your columns. Something like this, wired to the click events of your buttons, would work:
First, name your Grid:
<Grid Name="MyGrid">
Then, wire your buttons:
<Button Click="Button_Click" Grid.Row="1" Margin="4">Play</Button>
<Button Name="FullScreenButton" Click="FullScreenButton_Click" Grid.Row="1" Grid.Column="1" Margin="4">Full Screen</Button>
<Button Name="RestoreButton" Click="RestoreButton_Click" Grid.Row="1" Grid.Column="1" Margin="4" Visibility="Hidden">Restore</Button>
And, use handlers to change the grid:
Private Sub Button_Click(sender As Object, e As RoutedEventArgs)
meVid.Play()
End Sub
Private Sub FullScreenButton_Click(sender As Object, e As RoutedEventArgs)
MyGrid.ColumnDefinitions.Item(2).Width = New GridLength(0)
MyGrid.ColumnDefinitions.Item(3).Width = New GridLength(0)
FullScreenButton.Visibility = Windows.Visibility.Hidden
RestoreButton.Visibility = Windows.Visibility.Visible
End Sub
Private Sub RestoreButton_Click(sender As Object, e As RoutedEventArgs)
MyGrid.ColumnDefinitions.Item(2).Width = New GridLength(2, GridUnitType.Star)
MyGrid.ColumnDefinitions.Item(3).Width = New GridLength(1, GridUnitType.Star)
FullScreenButton.Visibility = Windows.Visibility.Visible
RestoreButton.Visibility = Windows.Visibility.Hidden
End Sub
That code will effectively toggle the widths of the columns you want to hide, to zero, and restore them to what you had defined in the XAML. Because you want to dynamically vary your Grid element sizes, you will need to do this in code someplace.
You can, of course, define this behavior in style triggers, or wire your size elements to ViewModel bindings, but those are topics all by themselves which might not pertain to your architecture. Since you defined XAML with code-behind behavior on the buttons, I put the code in the code-behind; the point is, you vary the size of your Grid ColumnDefinition and RowDefinition elements to hide them.
This avoids problems with templates or triggering but also introduces "separation of concerns" issues which may make your stuff harder to maintain, if it's part of a complex project.

Hiding/showing child controls when parent control gains/loses focus

I am creating a text editing control that contains a RichTextArea and a toolbar for formatting. I want the toolbar to only be visible when the control has focus. However I am finding this difficult to pull off in Silverlight, as Silverlight's focus API is very limited.
The control is structured like this:
<UserControl x:Class="MyRichTextBoxControl">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel x:Name="_formattingToolBar" Grid.Row="0" Orientation="Horizontal">
<Button Content="Bold" ... />
<!-- other buttons -->
</StackPanel>
<RichTextBox x:Name="_richTextBox" Grid.Row="1" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" />
</Grid>
</UserControl>
Initial Stab
At first I tried the obvious, I overrode OnGotFocus and OnLostFocus on the parent UserControl and hid/showed _formattingToolbar appropriately. This does not work, because if a child control gains focus, then Silverlight considers the parent control lost focus. The net result is trying to click on the toolbar causes it to disappear.
Nasty Solution
So far the only solution I have found is to hook up event handlers to the GotFocus and LostFocus events on every single child control and the parent UserControl. The event handler will call FocusManager.GetFocusedElement() and if the returned element is found to be a child of my UserControl, then keep _formattingToolbar visible, otherwise collapse it. I'm sure this will work, but it's pretty ugly.
It's also possible this idea is faulty because GotFocus/LostFocus are fired asynchronously, while GetFocusedElement() is determined synchronously. Could there be race conditions causing my idea to fail?
Anyone know of a better solution?
Nitin Midha, you had the right idea. That brought my back to my original attempt and a slight altering of OnLostFocus does the trick:
protected override void OnLostFocus(RoutedEventArgs e)
{
base.OnLostFocus(e);
if (!IsChild(FocusManager.GetFocusedElement()))
{
HideToolbar();
}
}
protected override void OnLostFocus(RoutedEventArgs e)
{
base.OnLostFocus(e);
object focusedElement = FocusManager.GetFocusedElement();
if (focusedElement is UIElement)
{
if (!this.LayoutRoot.Children.Contains((UIElement)focusedElement))
{
// Do your thing.
}
}
else { /**/ }
}

How to dynamically add MenuItems (with a header) to a WPF menu

[Edit #3] - to anyone reading this question: do not under any circumstance use the approach outlined in this question. It is a Coding Horror. I freely admit this, knowing that all programmers have worked themselves into a corner in the past, and (especially when learning a new technology) we all have been led astray by other, well-meaning developers on the interweb. Read the answer by Robert first, then read this question. Please.
[Edit #2b]
I apologize for the length of this question - there is a question in here (at the end!), but I wanted to make sure the source code was explicit. Anyway.
[Edit #2] - question title changed to more accurately reflect the... question.
[Edit] - I've updated some more of the history as to how I ended up at the design / code that I did here: Obligatory Blog Post. If it helps clarify the question below, feel free to read it...
Original question
The application I'm working on uses Prism and WPF, with a number of modules (currently 3), one of which hosts the application menu. Originally, the menu was static with hooks into CompositeCommand / DelegateCommands, which worked great for routing button presses to the appropriate presenter. Each MenuItem used a StackPanel in its header to display the content as a combination of an image and a text label - which was the look I was going for:
<Menu Height="48" Margin="5,0,5,0" Name="MainMenu" VerticalAlignment="Top" Background="Transparent">
<MenuItem Name="MenuFile" AutomationProperties.AutomationId="File">
<MenuItem.Header>
<StackPanel>
<Image Height="24" VerticalAlignment="Center" Source="../Resources/066.png"/>
<ContentPresenter Content="Main"/>
</StackPanel>
</MenuItem.Header>
<MenuItem AutomationProperties.AutomationId="FileExit" Command="{x:Static local:ToolBarCommands.FileExit}">
<MenuItem.Header>
<StackPanel>
<Image Height="24" VerticalAlignment="Center" Source="../Resources/002.png"/>
<ContentPresenter Content="Exit"/>
</StackPanel>
</MenuItem.Header>
</MenuItem>
</MenuItem>
<MenuItem Name="MenuHelp" AutomationProperties.AutomationId="Help" Command="{x:Static local:ToolBarCommands.Help}">
<MenuItem.Header>
<StackPanel>
<Image Height="24" VerticalAlignment="Center" Source="../Resources/152.png"/>
<ContentPresenter Content="Help"/>
</StackPanel>
</MenuItem.Header>
</MenuItem>
</Menu>
Unfortunately, the application has gotten a bit more complex and it is desireable to have other modules register themselves with the menu - hence, I've been looking at making the menu dynamic. The goal is to have other modules (through a service) be able to add commands to the menu at will - for example, Module A will add a menu item in the Toolbar module that calls a handler in Module A. There's a few excellent articles out there on this subject - the two I've looked at are Building a Databound WPF Menu Using a HierarchicalDataTemplate and WPF Sample Series - Databound HierarchicalDataTemplate Menu Sample. Following the advice in the article, I have managed to make a dynamically constructed menu with no obvious data binding problems - it can create a menu with items linked backed to my presentation model, reflecting the structure of an ObservableCollection in the presentation model
Currently, my XAML looks like the following:
<UserControl x:Class="Modules.ToolBar.Views.ToolBarView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:model="clr-namespace:Modules.ToolBar.PresentationModels"
xmlns:local="clr-namespace:Modules.ToolBar">
<UserControl.Resources>
<model:ToolBarPresentationModel x:Key="modelData" />
<HierarchicalDataTemplate DataType="{x:Type model:ToolbarObject}"
ItemsSource="{Binding Path=Children}">
<ContentPresenter Content="{Binding Path=Name}"
Loaded="ContentPresenter_Loaded"
RecognizesAccessKey="True"/>
</HierarchicalDataTemplate>
</UserControl.Resources>
<UserControl.DataContext>
<Binding Source="{StaticResource modelData}"/>
</UserControl.DataContext>
<Menu Height="48" Margin="5,0,5,0" Name="MainMenu" VerticalAlignment="Top" Background="Transparent"
ItemsSource="{Binding}">
</Menu>
</Grid>
</UserControl>
The code behind for the view does the heavy lifting in the ContentPresenter_Loaded method:
private void ContentPresenter_Loaded(object sender, System.Windows.RoutedEventArgs e)
{
ContentPresenter presenter = sender as ContentPresenter;
if (sender != null)
{
DependencyObject parentObject = VisualTreeHelper.GetParent(presenter);
bool bContinue = true;
while (bContinue
|| parentObject == null)
{
if (parentObject is MenuItem)
bContinue = false;
else
parentObject = VisualTreeHelper.GetParent(parentObject);
}
var menuItem = parentObject as MenuItem;
if (menuItem != null)
{
ToolbarObject toolbarObject = menuItem.DataContext as ToolbarObject;
StackPanel panel = new StackPanel();
if (!String.IsNullOrEmpty(toolbarObject.ImageLocation))
{
Image image = new Image();
image.Height = 24;
image.VerticalAlignment = System.Windows.VerticalAlignment.Center;
Binding sourceBinding = new Binding("ImageLocation");
sourceBinding.Mode = BindingMode.TwoWay;
sourceBinding.Source = toolbarObject;
image.SetBinding(Image.SourceProperty, sourceBinding);
panel.Children.Add(image);
}
ContentPresenter contentPresenter = new ContentPresenter();
Binding contentBinding = new Binding("Name");
contentBinding.Mode = BindingMode.TwoWay;
contentBinding.Source = toolbarObject;
contentPresenter.SetBinding(ContentPresenter.ContentProperty, contentBinding);
panel.Children.Add(contentPresenter);
menuItem.Header = panel;
Binding commandBinding = new Binding("Command");
commandBinding.Mode = BindingMode.TwoWay;
commandBinding.Source = toolbarObject;
menuItem.SetBinding(MenuItem.CommandProperty, commandBinding);
}
}
}
As you can see, I'm attempting to recreate the StackPanel / Image / Name combination of the original menu, just doing so in the code behind. Attempting to do this has not worked out so well - while the menu objects are certainly being created, they don't "appear" as anything other than blank, clickable objects - the StackPanel, Image, Name, etc. aren't being rendered. Interestingly enough, it also is causing the original text in the ContentPresent in the HierarchicalDataTemplate to be erased.
The question then, is there a way to set a MenuItem's Header property in the Load event such that it will display on the UserControl properly? Is the fact that the items in the header are not being displayed indicative of a DataBinding problem? If so, what would be the proper way to bind the Header to a transient object (the StackPanel that was created in the load event handler)?
I'm open to changing anything in the code above - this is all sort of prototyping along, trying to figure out the best way to handle dynamic menu creation.
Thanks!
I'll confess that I haven't dug quite as deep into your example as maybe I should, but whenever I see code-behind that's searching the visual tree, I think, could this be handled more explicitly in a view model?
It seems to me in this case that you could come up with a pretty straightforward view model - an object exposing Text, Image, Command, and Children properties, for instance - and then create a simple data template that for presenting it as a MenuItem. Then anything that needs to alter the contents of your menus manipulates this model.
Edit:
Having looked at what you're up to in more detail, and the two examples you've linked to in your blog post, I am banging my head against the desk. Both of those developers appear to be under the misapprehension that the way to set properties on the menu items that are being generated by the template is to search through the visual tree in the ContentPresenter.Load event after they're created. Not so. That's is what the ItemContainerStyle is for.
If you use that, it's quite straightforward to create dynamic menus of the type you're describing. You need a MenuItemViewModel class that has INotifyPropertyChanged implemented and exposes these public properties:
string Text
Uri ImageSource
ICommand Command
ObservableCollection<MenuItemViewModel> Children
Using this:
<Menu DockPanel.Dock="Top" ItemsSource="{DynamicResource Menu}"/>
where the ItemsSource is an ObservableCollection<MenuItemViewModel>, and using this template:
<HierarchicalDataTemplate DataType="{x:Type local:MenuItemViewModel}"
ItemsSource="{Binding Path=Children}">
<HierarchicalDataTemplate.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Command"
Value="{Binding Command}" />
</Style>
</HierarchicalDataTemplate.ItemContainerStyle>
<StackPanel Orientation="Horizontal">
<Image Source="{Binding ImageSource}" />
<Label Content="{Binding Text}" />
</StackPanel>
</HierarchicalDataTemplate>
the menus in the window exactly represent what's in the collection, and are dynamically updated as items are added and removed, both to the top-level items and to the descendants.
There's no clambering about in the visual tree, no manual creation of objects, no code-behind (other than in the view model, and in whatever populates the collection in the first place).
I've built a pretty thoroughly worked example of this; you can download the project here.
Another possible approach could be having the Menu be a region and agree on a convention so all views added to that region have a ViewModel with a property named MenuHeader. That way, the region adapter can simply get the menu header from the View's Data Context, and set it to the item when adding it.
Something similar is done in Prism with views added to a Tab Region. You can read more here.
I hope this provides some useful guidance.
Thanks,
Damian

Resources