WPF - Redrawing a Context Menu when Items change? - wpf

I have a ItemsControl in a ScrollViewer, and when the items exceed the width of the ScrollViewer they are put into a ContextMenu and shown as a DropDown instead. My problem is that when the Context Menu is first loaded, it saves the saves the size of the Menu and does not redraw when more commands get added/removed.
For example, a panel has 3 commands. 1 is visible and 2 are in the Menu. Viewing the menu shows the 2 commands and draws the control, but then if you resize the panel so 2 are visible and only 1 command is in the menu, it doesn't redraw the menu to eliminate that second menu item. Or even worse, if you shrink the panel so that no commands are shown and all 3 are in the Menu, it will only show the top 2.
Here's my code:
<Button Click="DropDownMenu_Click"
ContextMenuOpening="DropDownMenu_ContextMenuOpening">
<Button.ContextMenu>
<ContextMenu ItemsSource="{Binding Path=MenuCommands}" Placement="Bottom">
<ContextMenu.Resources>
<Style TargetType="{x:Type MenuItem}">
<Setter Property="Command" Value="{Binding Path=Command}" />
<Setter Property="Visibility" Value="{Binding Path=IsVisible, Converter={StaticResource ReverseBooleanToVisibilityConverter}}"/>
</Style>
</ContextMenu.Resources>
<ContextMenu.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=DisplayName}" />
</DataTemplate>
</ContextMenu.ItemTemplate>
</ContextMenu>
</Button.ContextMenu>
</Button>
Code Behind:
void DropDownMenu_ContextMenuOpening(object sender, ContextMenuEventArgs e)
{
Button b = sender as Button;
b.ContextMenu.IsOpen = false;
e.Handled = true;
}
private void DropDownMenu_Click(object sender, RoutedEventArgs e)
{
Button b = sender as Button;
ContextMenu cMenu = b.ContextMenu;
if (cMenu != null)
{
cMenu.PlacementTarget = b;
cMenu.Placement = System.Windows.Controls.Primitives.PlacementMode.Bottom;
cMenu.IsOpen = true;
}
}
I have tried using InvalidateVisual and passing an empty delegate on Render to try and force a redraw, however neither works. I'm using .Net 4.0.

Is MenuCommands a collection? If it is, is it an ObservableCollection?
If you bind a collection to an ItemsControl, that collection must implement INotifyCollectionChanged interface to let the ItemsControl know that the number of items in the collection has changed, so that the control can "redraw" itself.

Related

Non-stacking ItemsControl

There is probably a better way to do this, but here goes: I have a user control for each step of a user workflow process. I am using Microsoft Prism and dependency injection. I have an ItemsControl that is binded to a List. Anytime the user does an action that requires the next "step", I add the user control to this list, which the ItemsControl binds to.
Of course the user controls stack on top of eachother from top to bottom. What I want is to have the last one drawn be drawn ON TOP OF every other one. That way when the user is done with this step and hits "OK", I just remove the user control, and the previous step is automatically displayed.
I created a tab control, as per HighCore's idea.
<TabControl Name="MIPTabs" ItemsSource="{Binding UCList}" HorizontalAlignment="Left" VerticalAlignment="Top" Height="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type UserControl}}, Path=Height}" Width="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type UserControl}}, Path=Width}">
Each item in UCList is a UserControl, and its content gets automatically added to each new tab. I then hide the tabs with:
<TabControl.Resources>
<Style TargetType="TabItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TabItem">
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</TabControl.Resources>
Then I use the following code so that each time a tab is added, it will set the selected tab equal to the newest tab:
public MIP(IMIPVM viewModel) : this()
{
this.DataContext = viewModel;
var dpd = DependencyPropertyDescriptor.FromProperty(ItemsControl.ItemsSourceProperty, typeof(TabControl));
if (dpd != null)
{
dpd.AddValueChanged(MIPTabs, UpdateTabs);
}
}
private void UpdateTabs(object sender, EventArgs e)
{
var view = CollectionViewSource.GetDefaultView(MIPTabs.ItemsSource);
view.CollectionChanged += (o, f) =>
{
//This makes it so the last item ADDED is always the tab that is being displayed
MIPTabs.SelectedIndex = MIPTabs.Items.Count-1;
};
}

Expander inside ListBox not showing content when expanded the first time from binding

I have a dialog window that contains a ListBox whose ItemTemplate contains an expander.
Its IsExpanded is bound to a property in the item view model. The ListBoxItem's IsSelected property is also bound to the IsExpanded property in the item view model object. And finally the SelectedItem property of the ListBox is bound to a property with the same name in the view model.
The problem here is that when setting up the view model before showing the dialog and setting it to the DataContext of the dialog, the item in the listbox gets selected as it should, the expander arrow shows that it is in the expanded state, but the content of the expander is not displayed.
If I set up the view model after showing the dialog, eg. in the Loaded handler of the dialog things work as expected. What is going on here, and what would be the best way to fix it?
The dialog window is defined as:
<Window x:Class="WpfApplication1.Dialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:l="clr-namespace:WpfApplication1"
Title="Dialog" Height="300" Width="300">
<Grid>
<ListBox ItemsSource="{Binding Items}" SelectedItem="{Binding SelectedItem, Mode=TwoWay}">
<ListBox.ItemTemplate>
<DataTemplate>
<Expander Header="Expander" x:Name="MyExpander" IsExpanded="{Binding IsExpanded, Mode=TwoWay}">
<Rectangle Width="100" Height="20" Fill="Red" />
</Expander>
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="IsSelected" Value="{Binding IsExpanded, Mode=TwoWay}" />
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</Grid>
And the ViewModel (implementation not included for the sake of brevity):
public interface IMyViewModel : INotifyPropertyChanged
{
object SelectedItem { get; set; }
ObservableCollection<IMyItemViewModel> Items { get; }
}
public interface IMyItemViewModel : INotifyPropertyChanged
{
bool IsExpanded { get; set; }
}
Then I have a simple main window with a button, and its Click handler is defined as:
private void Button_Click(object sender, RoutedEventArgs e)
{
MyViewModel vm = new MyViewModel();
MyItemViewModel item = new MyItemViewModel();
vm.Items.Add(item);
vm.SelectedItem = item;
Dialog dialog = new Dialog();
dialog.DataContext = vm;
dialog.ShowDialog();
}
When I run the application and click the button, the dialog shows up, the expander arrow indicates that it is in the expanded state, but its content is not displayed. Clicking on the expander collapses it, and clicking it again expands it, this time showing the content.
Putting the same code directly in the Main Window instead of a dialog however works as it is supposed to.
If I just do a Dispatcher.BeginInvoke(new Action(() => vm.SelectedItem = item); instead of setting it directly things also seem to work, but this feels a bit shaky.
What can be done to fix this problem?
Sounds like the content of the expander is not measured again after it is loaded, if the IsExpanded property is already set to true. Or to put it another way, the content is measured when it has still no actual size.
I suppose the easiest solution would be to just set the SelectedItem once the dialog has been loaded:
dialog.Loaded += (s, x) => vm.SelectedItem = item;

Dynamic template for a ComboBox in ListBox

I have a ListBox with an embedded ComboBox:
<ListBox ItemsSource="{Binding}">
<ListBox.ItemTemplate>
<DataTemplate>
<ComboBox Width="100" IsEditable="False" Height="20">
<TextBlock Text="Opt#1"></TextBlock>
<TextBlock Text="Opt#2"></TextBlock>
<TextBlock Text="Opt#3"></TextBlock>
</ComboBox>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
I'd like to present the ComboBox as a simple text (e.g. TextBlock) when a ListBox row is not selected, and show it as a ComboBox when the ListBox row is selected.
I was thinking that replacing ComboBox template dynamically would do the trick. How to accomplish that?
Thanks,
Leszek
The best way to swap templates is to use the ItemTemplateSelector propery of the ListBox and set it to a class you create which inherits from DataTemplateSelector.
Here is a link that provides an example: http://msdn.microsoft.com/en-us/library/system.windows.controls.datatemplateselector.aspx
I would simply use a style that replace the ListBox.ItemTemplate whenever the ListBoxItem becomes selected.
Here's a quick example
<ListBox.Resources>
<DataTemplate x:Key="TextBoxTemplate">
<TextBlock Text="{Binding }" />
</DataTemplate>
<DataTemplate x:Key="ComboBoxTemplate">
<ComboBox SelectedItem="{Binding }">
<ComboBoxItem>Opt#1</ComboBoxItem>
<ComboBoxItem>Opt#2</ComboBoxItem>
<ComboBoxItem>Opt#3</ComboBoxItem>
</ComboBox>
</DataTemplate>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="Template" Value="{StaticResource TextBoxTemplate}" />
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Template" Value="{StaticResource ComboBoxTemplate}" />
</Trigger>
</Style.Triggers>
</Style>
</ListBox.Resources>
I'd actually suggest using IsKeyboardFocusWithin instead of IsSelected as the trigger property, because templates can let you interact with them without setting the item as selected.
Thanks Josh and Rachel for pointing me in a right direction.
I came up with a solution similar to the one suggested by Rachel. My problem was I could not make ItemTemplateSelector work and I did not know how to pass the state IsSelected from my listbox. I also could not use DataTemplate because my ListBox item is much more complex than a single element (I simplified it in my previous post for the sake of example).
Anyway, I came up with the following solution. It's not very elegant but it works:
I defined a new style in Application resources:
<Style x:Key="TextBlockTemplate" TargetType="ComboBox">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<TextBlock Text="{Binding}" Margin="3" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
I attached SelectionChanged and PreviewMouseDown handlers to my ListBox:
I defined MyListBox_PreviewMouseDown:
private void MyListBox_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
// Grab the selected list box item.
object element = (e.OriginalSource as FrameworkElement).DataContext;
var item = MyListBox.ItemContainerGenerator.ContainerFromItem(element)
as ListBoxItem;
// Mark the row in the ListBox as selected.
if (item != null)
item.IsSelected = true;
}
I defined MyListBox_SelectionChanged:
private ComboBox prevComboBox = null;
private void MyListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
// Grab the list box.
ListBox list = sender as ListBox;
// Although there could be only one item selected,
// we iterate over all selected items.
foreach (MyDataItem dat in list.SelectedItems)
{
var item = list.ItemContainerGenerator.ContainerFromItem(dat) as ListBoxItem;
// FindElement is a helper method to find an element in a visual tree.
ComboBox cbo = FindElement(item, "MyComboBox") as ComboBox;
if (cbo != prevComboBox)
{
cbo.Style = null;
if (prevComboBox != null)
prevComboBox.Style =
(Style)Application.Current.Resources["TextBlockTemplate"];
prevComboBox = cbo;
}
}
}
Thanks,
Leszek

WPF: How to disable tab navigation without also disabling arrow key navigation?

I have set IsTabStop to false on all controls in my window, so that when I press the Tab key, the focus doesn't move (I need the Tab key for something else). But doing this breaks arrow key navigation - I click on an item in a ListView and then pressing up/down doesn't change the selected item anymore.
Is there a way to disable tab navigation, but without touching arrow key navigation? They seem to be related.
I tried setting IsTabStop to true and TabNavigation to false, but it doesn't work either.
<ListView ItemContainerStyle="{StaticResource ItemCommon}" IsTabStop="False">
<ListView.Resources>
<Style x:Key="ItemCommon">
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="KeyboardNavigation.TabNavigation" Value="None"/>
<Setter Property="KeyboardNavigation.DirectionalNavigation" Value="Cycle"/>
</Style>
</ListView.Resources>
</ListView>
On your window (or some ancestor of the controls you don't want tab to work on) swallow the tab key.
You can swallow it by attaching to the PreviewKeyDown event and set e.Handled = true when the key is a tab.
Pure Code Behind:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.PreviewKeyDown += MainWindowPreviewKeyDown;
}
static void MainWindowPreviewKeyDown(object sender, KeyEventArgs e)
{
if(e.Key == Key.Tab)
{
e.Handled = true;
}
}
}
You can also set a Keyboard handler as such:
<Window x:Class="TabSwallowTest.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"
Keyboard.PreviewKeyDown="Window_PreviewKeyDown" >
<StackPanel>
<TextBox Width="200" Margin="10"></TextBox>
<TextBox Width="200" Margin="10"></TextBox>
</StackPanel>
</Window>
but you'll need a corresponding event handler:
private void Window_PreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Tab)
{
e.Handled = true;
}
}
I believe what you want is to set the KeyboardNavigation.TabNavigation attached property to Once on your ListView. I've done this with a templated ItemsControl and it seems to give me the behavior that I would expect from like a ListBox where a tab into the control will select the first item but an additional tab will tab right out of the listbox and onto the next control.
So following this method your example may be able to be shortend down to just this.
<ListView ItemContainerStyle="{StaticResource ItemCommon}"
KeyboardNavigation.TabNavigation="Once" />
I haven't tested this with the ListView control however but I wouldn't be surprised if it works for you.

How to get a group of toggle buttons to act like radio buttons in WPF?

I have a group of buttons that should act like toggle buttons, but also as radio buttons where only one button can be selected / pressed down at a current time. It also need to have a state where none of the buttons are selected / pressed down.
The behavior will be kind of like Photoshop toolbar, where zero or one of the tools are selected at any time!
Any idea how this can be implemented in WPF?
This is easiest way in my opinion.
<RadioButton Style="{StaticResource {x:Type ToggleButton}}" />
Enjoy!
-- Pricksaw
The easiest way is to style a ListBox to use ToggleButtons for its ItemTemplate
<Style TargetType="{x:Type ListBox}">
<Setter Property="ListBox.ItemTemplate">
<Setter.Value>
<DataTemplate>
<ToggleButton Content="{Binding}"
IsChecked="{Binding IsSelected, Mode=TwoWay, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListBoxItem}}}"
/>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
Then you can use the SelectionMode property of the ListBox to handle SingleSelect vs MultiSelect.
<RadioButton Content="Point" >
<RadioButton.Template>
<ControlTemplate>
<ToggleButton IsChecked="{Binding IsChecked, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
Content="{Binding Content, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"/>
</ControlTemplate>
</RadioButton.Template>
</RadioButton>
it works for me, enjoy!
you could always use a generic event on the Click of the ToggleButton that sets all ToggleButton.IsChecked in a groupcontrol(Grid, WrapPanel, ...) to false with the help of the VisualTreeHelper; then re-check the sender.
Or something in the likes of that.
private void ToggleButton_Click(object sender, RoutedEventArgs e)
{
int childAmount = VisualTreeHelper.GetChildrenCount((sender as ToggleButton).Parent);
ToggleButton tb;
for (int i = 0; i < childAmount; i++)
{
tb = null;
tb = VisualTreeHelper.GetChild((sender as ToggleButton).Parent, i) as ToggleButton;
if (tb != null)
tb.IsChecked = false;
}
(sender as ToggleButton).IsChecked = true;
}
you can put grid with radiobuttons in it, and create button like template for raduiobuttons. than just programmaticaly remove check if you don't want buttons to be toggled
You can also try System.Windows.Controls.Primitives.ToggleButton
<ToggleButton Name="btnTest" VerticalAlignment="Top">Test</ToggleButton>
Then write code against the IsChecked property to mimick the radiobutton effect
private void btnTest_Checked(object sender, RoutedEventArgs e)
{
btn2.IsChecked = false;
btn3.IsChecked = false;
}
I did this for RibbonToggleButtons, but maybe it's the same for regular ToggleButtons.
I bound the IsChecked for each button to a "mode" enum value using EnumToBooleanConverter from here How to bind RadioButtons to an enum? (Specify the enum value for this button using the ConverterParameter. You should have one enum value for each button)
Then to prevent unchecking a button that's already checked, put this in your code behind for the Click event for each of the RibbonToggleButtons:
private void PreventUncheckRibbonToggleButtonOnClick ( object sender, RoutedEventArgs e ) {
// Prevent unchecking a checked toggle button - so that one always remains checked
// Cancel the click if you hit an already-checked button
var button = (RibbonToggleButton)sender;
if( button.IsChecked != null ) { // Not sure why checked can be null but that's fine, ignore it
bool notChecked = ( ! (bool)button.IsChecked );
if( notChecked ){ // I guess this means the click would uncheck it
button.IsChecked = true;
}
}
}
To help people like Julian and me (two minutes ago...). You can derive from the RadioButton like this.
class RadioToggleButton : RadioButton
{
protected override void OnToggle()
{
if (IsChecked == true) IsChecked = IsThreeState ? (bool?)null : (bool?)false;
else IsChecked = IsChecked.HasValue;
}
}
Then, you can use it like Uday Kiran suggested...
<Window x:Class="Sample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Sample"
Title="MainWindow" Height="600" Width="600">
<StackPanel>
<local:RadioToggleButton Content="Button" Style="{StaticResource {x:Type ToggleButton}}" />
</StackPanel>
</Window>
This method allows only one ToggleButton to be Checked at a time, and it also allows UnChecking.
I took a few piece of the answers and added some extra code. Now you can have different groups of toggle buttons which act like one toggle button:
<UserControl.Resources>
<Style x:Key="GroupToggleStyle" TargetType="ToggleButton">
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding GroupName, RelativeSource={RelativeSource Self}}" Value="Group1"/>
<Condition Binding="{Binding BooleanProperty}" Value="true"/>
</MultiDataTrigger.Conditions>
<MultiDataTrigger.Setters>
<Setter Property="IsChecked" Value="true"/>
</MultiDataTrigger.Setters>
</MultiDataTrigger>
</Style.Triggers>
</Style>
</UserControl.Resources>
And the different groups of radio buttons which look like toggle buttons:
<Radio Button GroupName="Group1" Style="{StaticResource {x:Type ToggleButton}}">
<Radio Button GroupName="Group1" Style="{StaticResource {x:Type ToggleButton}}">
<Radio Button GroupName="Group2" Style="{StaticResource {x:Type ToggleButton}}">
<Radio Button GroupName="Group3" Style="{StaticResource {x:Type ToggleButton}}">
One simplistic implementation could be where you maintain a flag in your code behind such as:
ToggleButton _CurrentlyCheckedButton;
Then assign a single Checked event handler to all your context ToggleButtons:
private void ToggleButton_Checked(object sender, RoutedEventArgs e)
{
if (_CurrentlyCheckedButton != null)
_CurrentlyCheckedButton.IsChecked = false;
_CurrentlyCheckedButton = (sender as ToggleButton);
}
And, a single Unchecked event handler:
private void ToggleButton_Unchecked(object sender, RoutedEventArgs e)
{
if (_CurrentlyCheckedButton == (sender as ToggleButton))
_CurrentlyCheckedButton = null;
}
This way you can have the 'zero or one' selection you desire.

Resources