Hit-Testing in WPF for irregularly-shaped items - c#

I have an irregularly shaped item (a line shape) contained within a ContentControl-derived class ("ShapeItem"). I style it with a custom cursor and I handle mouse-clicks within the ShapeItem class.
Unfortunately WPF thinks that the mouse is "over" my item if it is anywhere within the rectangular bounding box of the ContentControl. That's OK for closed shapes like a rect or circle, but it's a problem for a diagonal line. Consider this image with 3 such shapes on display and their bounding boxes shown in white:
Even if I am in the very bottom left hand corner of the bounding box around the line, it still shows the cursor and the mouse clicks still reach my custom item.
I want to change this so that that the mouse is only considered to be "over" the line detected if I am within a certain distance of it. Like, this region in red (forgive the crude drawing).
My question is, how do I approach this? Do I override some virtual "HitTest" related function on my ShapeItem?
I already know the math to figure out if I'm in the right place. I'm just wondering what approach is the best to choose. What functions do I override? Or what events do I handle, etc. I've gotten lost in the WPF documentation on Hit testing. Is it a matter of overriding HitTestCore or something like that?
Now for code. I host the items in a custom ItemsControl called "ShapesControl".
which uses the custom "ShapeItem" container to to host my view-model objects :
<Canvas x:Name="Scene" HorizontalAlignment="Left" VerticalAlignment="Top">
<gcs:ShapesControl x:Name="ShapesControl" Canvas.Left="0" Canvas.Top="0"
ItemsSource="{Binding Shapes}">
<gcs:ShapesControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas Background="Transparent" IsItemsHost="True" />
</ItemsPanelTemplate>
</gcs:ShapesControl.ItemsPanel>
<gcs:ShapesControl.ItemTemplate>
<DataTemplate DataType="{x:Type gcs:ShapeVm}">
<Path ClipToBounds="False"
Data="{Binding RelativeGeometry}"
Fill="Transparent"/>
</DataTemplate>
</gcs:ShapesControl.ItemTemplate>
<!-- Style the "ShapeItem" container that the ShapesControl wraps each ShapeVm ine -->
<gcs:ShapesControl.ShapeItemStyle>
<Style TargetType="{x:Type gcs:ShapeItem}"
d:DataContext="{d:DesignInstance {x:Type gcs:ShapeVm}}"
>
<!-- Use a custom cursor -->
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Cursor" Value="SizeAll"/>
<Setter Property="Canvas.Left" Value="{Binding Path=Left, Mode=OneWay}"/>
<Setter Property="Canvas.Top" Value="{Binding Path=Top, Mode=OneWay}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type gcs:ShapeItem}">
<Grid SnapsToDevicePixels="True" Background="{TemplateBinding Panel.Background}">
<!-- First draw the item (i.e. the ShapeVm) -->
<ContentPresenter x:Name="PART_Shape"
Content="{TemplateBinding ContentControl.Content}"
ContentTemplate="{TemplateBinding ContentControl.ContentTemplate}"
ContentTemplateSelector="{TemplateBinding ContentControl.ContentTemplateSelector}"
ContentStringFormat="{TemplateBinding ContentControl.ContentStringFormat}"
HorizontalAlignment="{TemplateBinding Control.HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding Control.VerticalContentAlignment}"
IsHitTestVisible="False"
SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}"
RenderTransformOrigin="{TemplateBinding ContentControl.RenderTransformOrigin}"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</gcs:ShapesControl.ShapeItemStyle>
</gcs:ShapesControl>
</Canvas>
My "ShapesControl"
public class ShapesControl : ItemsControl
{
protected override bool IsItemItsOwnContainerOverride(object item)
{
return (item is ShapeItem);
}
protected override DependencyObject GetContainerForItemOverride()
{
// Each item we display is wrapped in our own container: ShapeItem
// This override is how we enable that.
// Make sure that the new item gets any ItemTemplate or
// ItemTemplateSelector that might have been set on this ShapesControl.
return new ShapeItem
{
ContentTemplate = this.ItemTemplate,
ContentTemplateSelector = this.ItemTemplateSelector,
};
}
}
And my "ShapeItem"
/// <summary>
/// A ShapeItem is a ContentControl wrapper used by the ShapesControl to
/// manage the underlying ShapeVm. It is like the the item types used by
/// other ItemControls, including ListBox, ItemsControls, etc.
/// </summary>
[TemplatePart(Name="PART_Shape", Type=typeof(ContentPresenter))]
public class ShapeItem : ContentControl
{
private ShapeVm Shape => DataContext as ShapeVm;
static ShapeItem()
{
DefaultStyleKeyProperty.OverrideMetadata
(typeof(ShapeItem),
new FrameworkPropertyMetadata(typeof(ShapeItem)));
}
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
// Toggle selection when the left mouse button is hit
base.OnMouseLeftButtonDown(e);
ShapeVm.IsSelected = !ShapeVm.IsSelected;
e.Handled = true;
}
internal ShapesControl ParentSelector =>
ItemsControl.ItemsControlFromItemContainer(this) as ShapesControl;
}
The "ShapeVm" is just an abstract base class for my view models. Roughly this:
public abstract class ShapeVm : BaseVm, IShape
{
public virtual Geometry RelativeGeometry { get; }
public bool IsSelected { get; set; }
public double Top { get; set; }
public double Left { get; set; }
public double Width { get; }
public double Height { get; }
}

You could use a ShapeItem class like shown below. It is a Canvas with two Path children, one for hit testing and one for display. It resembles a few of the typical Shape properties (which you may extend according to your needs).
public class ShapeItem : Canvas
{
public ShapeItem()
{
var path = new Path
{
Stroke = Brushes.Transparent,
Fill = Brushes.Transparent
};
path.SetBinding(Path.DataProperty,
new Binding(nameof(Data)) { Source = this });
path.SetBinding(Shape.StrokeThicknessProperty,
new Binding(nameof(HitTestStrokeThickness)) { Source = this });
Children.Add(path);
path = new Path();
path.SetBinding(Path.DataProperty,
new Binding(nameof(Data)) { Source = this });
path.SetBinding(Shape.FillProperty,
new Binding(nameof(Fill)) { Source = this });
path.SetBinding(Shape.StrokeProperty,
new Binding(nameof(Stroke)) { Source = this });
path.SetBinding(Shape.StrokeThicknessProperty,
new Binding(nameof(StrokeThickness)) { Source = this });
Children.Add(path);
}
public static readonly DependencyProperty DataProperty =
Path.DataProperty.AddOwner(typeof(ShapeItem));
public static readonly DependencyProperty FillProperty =
Shape.FillProperty.AddOwner(typeof(ShapeItem));
public static readonly DependencyProperty StrokeProperty =
Shape.StrokeProperty.AddOwner(typeof(ShapeItem));
public static readonly DependencyProperty StrokeThicknessProperty =
Shape.StrokeThicknessProperty.AddOwner(typeof(ShapeItem));
public static readonly DependencyProperty HitTestStrokeThicknessProperty =
DependencyProperty.Register(nameof(HitTestStrokeThickness), typeof(double), typeof(ShapeItem));
public Geometry Data
{
get => (Geometry)GetValue(DataProperty);
set => SetValue(DataProperty, value);
}
public Brush Fill
{
get => (Brush)GetValue(FillProperty);
set => SetValue(FillProperty, value);
}
public Brush Stroke
{
get => (Brush)GetValue(StrokeProperty);
set => SetValue(StrokeProperty, value);
}
public double StrokeThickness
{
get => (double)GetValue(StrokeThicknessProperty);
set => SetValue(StrokeThicknessProperty, value);
}
public double HitTestStrokeThickness
{
get => (double)GetValue(HitTestStrokeThicknessProperty);
set => SetValue(HitTestStrokeThicknessProperty, value);
}
}
public class ShapeItemsControl : ItemsControl
{
protected override DependencyObject GetContainerForItemOverride()
{
return new ShapeItem();
}
protected override bool IsItemItsOwnContainerOverride(object item)
{
return item is ShapeItem;
}
}
You would use it an XAML like this:
<gcs:ShapeItemsControl ItemsSource="{Binding Shapes}">
<gcs:ShapeItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</gcs:ShapeItemsControl.ItemsPanel>
<gcs:ShapeItemsControl.ItemContainerStyle>
<Style TargetType="gcs:ShapeItem">
<Setter Property="Data" Value="{Binding RelativeGeometry}"/>
<Setter Property="Fill" Value="AliceBlue"/>
<Setter Property="Stroke" Value="Yellow"/>
<Setter Property="StrokeThickness" Value="3"/>
<Setter Property="HitTestStrokeThickness" Value="15"/>
<Setter Property="Cursor" Value="Hand"/>
</Style>
</gcs:ShapeItemsControl.ItemContainerStyle>
</gcs:ShapeItemsControl>
However, you may not need a ShapeItem class and a derived ItemsControl at all, when you put the Canvas in the ItemTemplate of a regular ItemsControl:
<ItemsControl ItemsSource="{Binding Shapes}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Canvas Cursor="Hand">
<Path Data="{Binding RelativeGeometry}" Fill="Transparent"
Stroke="Transparent" StrokeThickness="15"/>
<Path Data="{Binding RelativeGeometry}" Fill="AliceBlue"
Stroke="Yellow" StrokeThickness="3"/>
</Canvas>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
If you also need to support selection, you should use a ListBox instead of an ItemsControl. A third Path in the ItemTemplate could visualize the selection state.
<ListBox ItemsSource="{Binding Shapes}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.Template>
<ControlTemplate TargetType="ListBox">
<ItemsPresenter/>
</ControlTemplate>
</ListBox.Template>
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="IsSelected" Value="{Binding IsSelected}"/>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<Canvas Cursor="Hand">
<Path Data="{Binding RelativeGeometry}" Fill="Transparent"
Stroke="Transparent" StrokeThickness="15"/>
<Path Data="{Binding RelativeGeometry}"
Stroke="Green" StrokeThickness="7"
StrokeStartLineCap="Square" StrokeEndLineCap="Square"
Visibility="{Binding IsSelected,
RelativeSource={RelativeSource AncestorType=ListBoxItem},
Converter={StaticResource BooleanToVisibilityConverter}}"/>
<Path Data="{Binding RelativeGeometry}" Fill="AliceBlue"
Stroke="Yellow" StrokeThickness="3"/>
</Canvas>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>

Related

WPF Listview Item Vertical Positioning with Canvas Overlap Issue

I have a word addin which has a WPF custom task pane. In that I have a Listview. I need to dynamicaly update the position of each listview item when word document scroll down. but some of them can be having same canvas.top(vertical Y) value.Then those items getting overlap.
I do not need to overlap those,I need as listview need to align one after another.
screen sample
Code Sample XAML..
<listViewTool:ListView x:Name="Results" Margin="0" BorderThickness="0" DockPanel.Dock="Top" Background="WhiteSmoke"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ItemsSource="{Binding Results}"
SelectionMode="Single">
<listViewTool:ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="Control.HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="Control.Margin" Value="0"/>
<Setter Property="Control.Padding" Value="0"/>
<Setter Property="Control.BorderThickness" Value="0"/>
<Setter Property="IsSelected" Value="{Binding Path=Selected, Mode=TwoWay}"/>
<Setter Property="Canvas.Top" Value="{Binding Y}"/>
<Setter Property="Canvas.Left" Value="0"/>
<Setter Property="Canvas.Width" Value="{Binding RelativeSource={RelativeSource FindAncestor,AncestorType={x:Type ListView}},Path=ActualWidth}" />
</Style>
</listViewTool:ListView.ItemContainerStyle>
<listViewTool:ListView.ItemsPanel>
<ItemsPanelTemplate>
<Canvas x:Name="CanvasMain" HorizontalAlignment="Stretch" Height="{Binding RelativeSource={RelativeSource FindAncestor,AncestorType={x:Type ListView}},Path=ActuaHeight}"
ClipToBounds="True" />
</ItemsPanelTemplate>
</listViewTool:ListView.ItemsPanel>
</listViewTool:ListView>
Thanks.
I've made a simple solution, which looks like this:
Idea is to store height of items and recalculate position of all items when height of either is changed.
Item height is item template border ActualHeight. For each item: Y is initial position, Offset is calculated/used to bind (based on Height of items before current item).
Solution contains some debugging code (Text property and binding) and is not a pure MVVM (but you didn't ask for one):
xaml:
<ListView ItemsSource="{Binding Items}">
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="Canvas.Left"
Value="0" />
<Setter Property="Canvas.Top"
Value="{Binding Offset}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListViewItem">
<Border CornerRadius="10"
BorderThickness="1"
BorderBrush="Gray"
SizeChanged="Border_SizeChanged">
<Expander Header="{Binding Text}">
<Grid Height="50" />
</Expander>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListView.ItemContainerStyle>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<Canvas Height="{Binding ActualHeight, RelativeSource={RelativeSource FindAncestor, AncestorType=ListView}}"
ClipToBounds="True" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListView>
cs:
public class Item : INotifyPropertyChanged
{
// INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged([CallerMemberName] string property = "") =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
public double Y { get; set; }
public double Height { get; set; }
double _offset;
public double Offset
{
get { return _offset; }
set
{
_offset = value;
OnPropertyChanged();
OnPropertyChanged(nameof(Text));
}
}
public string Text => $"Y={Y} Height={Height} Offset={Offset}";
}
public partial class MainWindow : Window
{
public ObservableCollection<Item> Items { get; } = new ObservableCollection<Item>();
public MainWindow()
{
InitializeComponent();
for (int i = 0; i < 10; i++)
Items.Add(new Item { Y = i * 40 });
DataContext = this;
}
void Border_SizeChanged(object sender, SizeChangedEventArgs e)
{
var border = (Border)sender;
var current = (Item)border.DataContext;
current.Height = border.ActualHeight;
// recalculate offset
var y = Items[0].Y;
foreach (var item in Items)
{
item.Offset = y > item.Y ? y : item.Y;
y = item.Offset + item.Height;
}
}
}

Positioning XAMLUI elements on ItemsControl with Canvas

I'm building a Windows Universal application where I need to put some elements on a canvas. Since I want to do this MVVM style I've read that I should use an ItemsControl for it. Problem is that all the solutions I found are for WPF or Silverlight and don't work for Windows Universal. They appear on the canvas, but they keep positioning themselfs in the left upper corner.
XAML:
<ItemsControl ItemsSource="{Binding MyItems}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Text}" FontSize="18"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding Left}" />
<Setter Property="Canvas.Top" Value="{Binding Top}" />
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
XAML's code behind:
public MainPage()
{
this.InitializeComponent();
DataContext = new MyItemViewModel();
}
Viewmodel and model:
class MyItemViewModel
{
ObservableCollection<MyItem> _myItems;
public MyItemViewModel()
{
_myItems = new ObservableCollection<MyItem>();
_myItems.Add(new MyItem(10, 10, "First TextBlock"));
_myItems.Add(new MyItem(200, 400, "2nd TextBlock"));
_myItems.Add(new MyItem(400, 200, "The Third TextBlock"));
}
public ObservableCollection<MyItem> MyItems
{
get { return _myItems; }
set { _myItems = value; }
}
}
public class MyItem
{
public MyItem(double left, double top, string text)
{
Left = left;
Top = top;
Text = text;
}
public double Left { get; set; }
public double Top { get; set; }
public string Text { get; set; }
}
Use FrameworkElement as the TargetType instead:
<ItemsControl.ItemContainerStyle>
<Style TargetType="FrameworkElement">
<Setter Property="Canvas.Left" Value="{Binding Left}" />
<Setter Property="Canvas.Top" Value="{Binding Top}" />
</Style>
</ItemsControl.ItemContainerStyle>

Button using dependency properties for a separate image per state

I have a Button control that I want to be able to reuse throughout project. Each time button enters a new state, a different image will be displayed. For now, I have Normal State and Pressed State.
Here's the XAML portion of the control:
<Button
x:Class="customImageButton.ImageButton"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Button.Template>
<ControlTemplate TargetType="{x:Type Button}">
<Grid>
<ContentControl Width="80">
<Grid>
<Image Name="Normal" Source="{Binding NormalState}"/>
<Image Name="Pressed" Source="{Binding PressedState}" Visibility="Hidden"/>
</Grid>
</ContentControl>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="Normal" Property="Visibility" Value="Hidden"/>
<Setter TargetName="Pressed" Property="Visibility" Value="Visible"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Button.Template>
</Button>
Here's the code-behind for the control:
namespace customImageButton
{
public partial class ImageButton : Button
{
public ImageButton()
{
this.InitializeComponent();
}
public ImageSource NormalState
{
get { return base.GetValue(NormalStateProperty) as ImageSource; }
set { base.SetValue(NormalStateProperty, value); }
}
public static readonly DependencyProperty NormalStateProperty =
DependencyProperty.Register("NormalState", typeof(ImageSource), typeof(ImageButton));
public ImageSource PressedState
{
get { return base.GetValue(PressedStateProperty) as ImageSource; }
set { base.SetValue(PressedStateProperty, value); }
}
public static readonly DependencyProperty PressedStateProperty =
DependencyProperty.Register("PressedState", typeof(ImageSource), typeof(ImageButton));
}
}
...and here is its use:
<local:ImageButton Content="CustomButton" HorizontalAlignment="Left"
VerticalAlignment="Top" NormalState="Resources/Normal.png"
PressedState="Resources/Pressed.png"/>
My problem is that the images I've provided are not displaying. The Build Action for both images is Resource and I have tried using absolute path; however, that provided the same result. What am I missing?
Two problems with the code as listed:
The bindings need to be TemplateBinding.
The TargetType should refer to the "ImageButton" type, not Button.
Like this:
<ControlTemplate
xmlns:local="clr-namespace:customImageButton"
TargetType="{x:Type local:ImageButton}">
<Grid>
<ContentControl Width="80">
<Grid>
<Image Name="Normal" Source="{TemplateBinding NormalState}"/>
<Image Name="Pressed" Source="{TemplateBinding PressedState}" Visibility="Hidden"/>
</Grid>
</ContentControl>
</Grid>
Note: The above builds and runs, but Visual Studio complains:
'ImageButton' ControlTemplate TargetType does not match templated type 'Button'.
I suggest putting the control template in its own Style in the Themes/Generic.xaml resource dictionary, rather than inline.

WPF ComboBox selection change after switching tabs

I made a project based on nested tabs.
the nested tabs are different instance of the same viemModel and the same UI.
when I switch between the tabs he comboboxes present in the tabs chenge thei selection depending on the tab that is loosing focus.
I add both the viewmodels and the view of my test project.
thank you in advance for your help
main window
<Window.Resources>
<DataTemplate DataType="{x:Type local:IntermediateViewModel}">
<local:IntermediateView />
</DataTemplate>
<DataTemplate x:Key="HeaderedTabItemTemplate">
<Grid>
<ContentPresenter
Content="{Binding Path=Header, UpdateSourceTrigger=PropertyChanged}"
VerticalAlignment="Center" >
</ContentPresenter>
</Grid>
</DataTemplate>
<Style x:Key="SimpleTabItemStyle" TargetType="TabItem">
<Setter Property="Foreground" Value="White"/>
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TabItem}">
<Grid>
<Border Name="Border" BorderThickness="1" BorderBrush="#555959">
<ContentPresenter x:Name="ContentSite" VerticalAlignment="Center" HorizontalAlignment="Center"
ContentSource="Header" Margin="12,2,12,2" RecognizesAccessKey="True" Height ="40" MinWidth ="90"/>
</Border>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="Border" Property="Background" Value="#555959" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<DataTemplate x:Key="DefaultTabControlTemplate">
<TabControl IsSynchronizedWithCurrentItem="True"
BorderThickness="0"
ItemsSource="{Binding}"
ItemTemplate="{StaticResource HeaderedTabItemTemplate}"
ItemContainerStyle="{StaticResource SimpleTabItemStyle}"
SelectionChanged="TabControl_SelectionChanged"
/>
</DataTemplate>
<!---->
</Window.Resources>
<Grid MinHeight="200" MinWidth="300">
<Grid.RowDefinitions>
<RowDefinition Height="260*" />
<RowDefinition Height="51*" />
</Grid.RowDefinitions>
<Border >
<ContentControl
Content="{Binding Path=Workspaces}"
ContentTemplate="{DynamicResource DefaultTabControlTemplate}"
/>
</Border>
<Button Grid.Row="1" Content="Add" Command="{Binding AddCommand}"/>
</Grid>
view model (create a different istance each time)
class MainWindowViewModel : WorkspacesViewModel<IntermediateViewModel>
{
public MainWindowViewModel()
{
this.WorkspacesView.CurrentChanged += new EventHandler(WorkspacesView_CurrentChanged);
}
void WorkspacesView_CurrentChanged(object sender, EventArgs e)
{
}
RelayCommand myVar = null;
public ICommand AddCommand
{
get
{
return myVar ?? (myVar = new RelayCommand(param =>
{
SetWindow(new IntermediateViewModel("AA" + this.Workspaces.Count) );
}));
}
}
first level tab
<UserControl.Resources>
<DataTemplate DataType="{x:Type local:ClassViewModel}">
<local:ClassView />
</DataTemplate>
</UserControl.Resources>
<Border>
<ContentControl Content="{Binding Path=CurrentWorkspace, Mode=OneWay}" Loaded="ContentControl_Loaded" DataContextChanged="ContentControl_DataContextChanged" IsVisibleChanged="ContentControl_IsVisibleChanged" LayoutUpdated="ContentControl_LayoutUpdated" TargetUpdated="ContentControl_TargetUpdated" Unloaded="ContentControl_Unloaded" />
</Border>
first level viewmodel
class IntermediateViewModel : WorkspacesViewModel
{
public string Header { get; set; }
public IntermediateViewModel(string header)
{
Header = header;
SetWindow(new ClassViewModel(header));
}
}
nested tab
<UserControl.Resources>
<CollectionViewSource x:Key="StatusView" Source="{Binding Path=StatusList}"/>
</UserControl.Resources>
<Grid>
<ComboBox Name="_spl2Status" ItemsSource="{Binding Source={StaticResource StatusView}}"
SelectedValue="{Binding Path=MyProperty, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
SelectedValuePath="FL_TYPE"
DisplayMemberPath="ID_TYPE" Margin="76,12,0,0" Height="40" VerticalAlignment="Top" HorizontalAlignment="Left" Width="146"
DataContextChanged="_spl2Status_DataContextChanged"
IsVisibleChanged="_spl2Status_IsVisibleChanged"
Loaded="_spl2Status_Loaded"
SelectionChanged="_spl2Status_SelectionChanged"
>
</ComboBox>
</Grid>
nested tab view model
public enum myTypes
{
tipo0 = 0,
tipo1 = 1,
tipo2 = 2,
}
class ClassViewModel : WorkspaceViewModel
{
public ClassViewModel(string name)
{
Name = name;
}
public string Name { get; set; }
private List<IntEnumType> _statusList = null;
public List<IntEnumType> StatusList
{
get
{
if (_statusList == null)
_statusList = new List<IntEnumType>()
{
new IntEnumType((int)myTypes.tipo0, myTypes.tipo0.ToString()),
new IntEnumType((int)myTypes.tipo1, myTypes.tipo1.ToString()),
new IntEnumType((int)myTypes.tipo2, myTypes.tipo2.ToString()),
};
return _statusList;
}
}
private int myVar = 1;
public int MyProperty
{
get
{
return myVar;
}
set
{
if (myVar != value)
{
myVar = value;
OnPropertyChanged(() => MyProperty);
}
}
}
}
public class TabItemStyleSelector : StyleSelector
{
public Style MainTabItem { get; set; }
public Style ChildrenTabItem { get; set; }
public Style SpecificationTabItem { get; set; }
public override Style SelectStyle(object item, DependencyObject container)
{
//if (item is IHome)
// return MainTabItem;
//else if (item is SpecificationItemViewModel)
// return SpecificationTabItem;
//else
return ChildrenTabItem;
}
}
The code is a little hard to completely follow, but I'm guessing that the issue is that there is only one instance of your ClassViewModel and it is where the selection for the combo box is stored {Binding Path=MyProperty, so whatever is stored in MyProperty will be reflected in all instances of the combo box regardless of where they live.
Well this is a bit late, but as I'm facing the same issue, I want to share my analysis.
When you change your tabs, you change the DataContext of the current Tab to your other ViewModel and hence also the ItemsSource of your ComboBox.
In case your previously selected Item (SelectedItem) is not contained within the new ItemsSource, the ComboBox fires a SelectionChanged-Event and therefore sets the SelectedIndex to -1.
Altough this default behaviour of the ComboBox might make sense, it's very annoying in many cases.
We've derived an own class from ComboBox, handling that. But it's not very satisfying as you loose some default behaviour you most probably need.
The problem is in your loaded event handlers.
When you switch tabs your unloading one tab and loading a new one.
I suspect your changing MyComboBox.SelectedIndex in _spl2Status_Loaded.

unable to get, set selectedItem from the listbox containing radiobuttons being generated dynamically in wpf MVVM

Am trying to populate listbox with dynamic radiobuttons which are being customized to togglebuttons. I could populate listbox items with radiobuttons as said above. However, once we select any of the radiobuttons am unable to set the selected item from list of radiobuttons in my viewmodel object while debugging.
The following is the xaml code in my resource directory
<Style x:Key="ScreensList" TargetType="{x:Type ListBox}">
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="ItemContainerStyle">
<Setter.Value>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="Margin" Value="2, 2, 2, 0" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Border Background="Transparent">
<RadioButton
VerticalAlignment="Center" GroupName="{Binding RelativeSource={RelativeSource TemplatedParent}}"
IsChecked="{Binding Path=IsSelected,RelativeSource={RelativeSource TemplatedParent},Mode=TwoWay}">
<RadioButton.Template>
<ControlTemplate>
<StackPanel Orientation="Horizontal">
<ToggleButton IsChecked="{Binding IsChecked, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}">
<StackPanel Width="80" Height="60" Orientation="Vertical" HorizontalAlignment="Left" Margin="10,10,20,10">
<Image Source="Default.png" Height="40"></Image>
<TextBlock Text="{Binding Path=ScreenNumber}" FontSize="11"></TextBlock>
</StackPanel>
</ToggleButton>
</StackPanel>
</ControlTemplate>
</RadioButton.Template>
</RadioButton>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Setter.Value>
</Setter>
</Style>
the below is my xaml code in xaml page
<ListBox x:Name="ScreensList" ItemsSource="{Binding ScreenCollection}"
SelectedItem="{Binding Path=ScreenManager}"
Style="{StaticResource ScreensList}" Width="365">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel FlowDirection="LeftToRight" Orientation="Horizontal" >
</WrapPanel>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
the following is the viewmodel.cs
public ObservableCollection<ScreensManager> ScreenCollection { get; set; }
private ScreensManager _screenManager;
public ScreensManager ScreenManager
{
get { return _screenManager; }
set
{
if (_screenManager != value)
{
if (_screenManager != null)
{
_screenManager = value;
}
}
}
}
private void AddScreens()
{
int screenCount = Screen.AllScreens.Length;
if (ScreenCollection == null)
ScreenCollection = new ObservableCollection<ScreensManager>();
for (int screenCounter = 1; screenCounter <= screenCount; screenCounter++)
{
if (screenCounter == 1)
{
_screenManager = new ScreensManager();
_screenManager.ScreenNumber = screenCounter;
ScreenCollection.Add(_screenManager);
}
}
}
the following is the code in my ScreenManager.cs model class file
public ScreensManager()
{
}
private int _screenNumber;
public int ScreenNumber
{
get { return _screenNumber; }
set
{
_screenNumber = value;
OnPropertyChanged("ScreenNumber");
}
}
private bool _selectedScreen;
public bool SelectedScreen
{
get { return _selectedScreen; }
set
{
if (_selectedScreen = value)
{
_selectedScreen = value;
if (_selectedScreen != value)
{
OnPropertyChanged("SelectedScreen");
}
}
}
}
private void OnPropertyChanged(string propName)
{
if (this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
public event PropertyChangedEventHandler PropertyChanged;
Am unable to find where I am actually going wrong as completely new to mvvm, someone please help me resolve my issue..Thanks in advance.
Your SelectedScreen is of type bool, and should be of type ScreensManager
Your ListBox.ItemsSource is bound to an ObservableCollection<ScreensManager>, meaning your ListBox contains a collection of ScreensManager objects, however your SelectedItem is of type bool. A bool object is never equal to a ScreensManager object, so WPF doesn't select anything since the SelectedItem is not found in the ItemsSource.
Change the SelectedScreen type to be ScreensManager instead of bool, and be sure it is equal to an item that exists in the ScreenCollection. WPF compares objects by reference, not value, so
ScreenManager.SelectedScreen = ScreenCollection.FirstOrDefault(); // Works
ScreenManager.SelectedScreen = new ScreensManager() { ... }; // Won't work
Make sure the binding is in two-way mode:
SelectedItem="{Binding Path=ScreenManager.SelectedScreen, Mode=TwoWay}"

Categories