How to get MVVM Mouse Position Inside of TabControl.ContentTemplate to ViewModel? - c#

I am creating a simple image editing-style app and am attempting to use MVVM for the first time. The idea is to parse proprietary text files and represent them visually in tabs within the app so they can be updated with paint strokes and the like before being converted back to text. In figuring out how to get mouse position to the view model, I found Mark Green's solution works well; however, I can't get it to work if I place it inside my TabControl.ContentTemplate - if I do so, the program crashes unceremoniously when I load the first text file (which creates the first tab). Here's the relevant section of code (see link for the code used in the ViewModels):
<Grid>
<!--If I stick the Interaction behavior here it works, but I get mouse position relative to entire grid-->
<TabControl ItemsSource="{Binding Maps}" SelectedIndex='{Binding TabIndex}'>
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Header}" />
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<Image x:Name="imgMap" Source='{Binding Content}'>
<i:Interaction.Behaviors><!--Putting it here causes an unhandled exception-->
<local:MouseBehaviour MouseX="{Binding PanelX, Mode=OneWayToSource}"
MouseY="{Binding PanelY, Mode=OneWayToSource}" />
</i:Interaction.Behaviors>
</Image>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</Grid>
As you can see from my comments in the code, my placement of the interaction behavior seems to be what is breaking the app. Since the exception occurs in external code, I can't figure out exactly what is wrong but I suspect it has something to do with the code being inside a template (please correct me if I am wrong!).
My question is this: how do I get interaction behaviors (and later triggers - I will want mouse button events as well) working within the template, or am I going about this the wrong way? Is there a better solution to get mouse position (relative to the image) to the ViewModel?
Many thanks in advance for any help offered!
UPDATE
Per STrenat's suggestion, I updated the MouseBehavior class to use UIElement instead of Panel:
public class MouseBehaviour : System.Windows.Interactivity.Behavior<UIElement>
{
// Rest of code from Mark Green's solution linked in original post
}
To get access to the mouse position values in the main window (instead of inside the ItemTemplate), I updated my bindings in the XAML (with output text blocks now shown) to point to properties in the main viewmodel (without the relative reference you have to have the properties inside the TabItem):
<StackPanel>
<TextBlock Text="{Binding PanelX, StringFormat='X={0:0}, '}" />
<TextBlock Text="{Binding PanelY, StringFormat='Y={0:0}'}" />
<TabControl ItemsSource="{Binding Maps}"
SelectedIndex='{Binding TabIndex}'>
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Header}" />
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<Image x:Name="imgMap"
Source='{Binding Content}'>
<i:Interaction.Behaviors>
<local:MouseBehaviour MouseX="{Binding DataContext.PanelX, RelativeSource={RelativeSource AncestorType={x:Type Window}}, Mode=OneWayToSource}"
MouseY="{Binding DataContext.PanelY, RelativeSource={RelativeSource AncestorType={x:Type Window}}, Mode=OneWayToSource}" />
</i:Interaction.Behaviors>
</Image>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</StackPanel>
The rest is as specified in the solution referenced above. Hope that helps anybody else running into this problem!

Related

Navigation between page

I'm writing a small WPF app with only 3 pages (for now). I'm using DataTemplate and ContentControl in the main window to display and switch between my pages. (See code sample below). It's working but I have a few concerns:
The DataTemplate use parameterless constructor only. If I add one then It can't find the constructor.
The 'registration' is done in the xaml and I cannot use Dependency Injection to link Views with ViewModels.
Questions:
Is there a way to change that without using third party tool?
If the only good option is to use a tool, which ones should I consider?
<Window.Resources>
<DataTemplate DataType="{x:Type pageViewModels:HomePageViewModel}">
<pageViews:HomePageView />
</DataTemplate>
<DataTemplate DataType="{x:Type pageViewModels:GamePageViewModel}">
<pageViews:GamePageView />
</DataTemplate>
</Window.Resources>
<DockPanel>
<Border DockPanel.Dock="Left" BorderBrush="Black" BorderThickness="0,0,1,0">
<ItemsControl ItemsSource="{Binding PageViewModels}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Content="{Binding Name}"
Command="{Binding DataContext.ChangePageCommand, RelativeSource={RelativeSource AncestorType={x:Type Window}}}"
CommandParameter="{Binding }"
Margin="2,5"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Border>
Edit
To clarify, I want to inject a class in the constructor of my viewsModels, but if I do that then the navigation within my application is broken because the dataTemplate is looking for the parameterless constructor.
It looks like my problem has been very well explained in this post.
In short, I need to implement a ViewModelLocator pattern in order to fix all my concerns.

have stackpanel clickable and load viewport

first post here so if I do anything wrong please slap me.
I'm currently trying to learn UWP code and I want to try to create a bottom naivation bar. I'm trying to work with stackpanels to do so
The idea is that when a stackpanel is clicked / tapped it should load a new page into viewport, however I can't figure out the correct code to do so. Here's an short version of my stackpanel:
<StackPanel Orientation="Horizontal" VerticalAlignment="Bottom">
<StackPanel Name="FirstButton" Tapped="FirstButton_Tapped">
<TextBlock x:Name="Icon1" />
<TextBlock x:Name="Text1" />
</StackPanel>
<StackPanel Name="SecondButton" Tapped="SecondButton_Tapped">
<TextBlock x:Name="Icon2" />
<TextBlock x:Name="Text2" />
</StackPanel>
<StackPanel Name="ThirdButton" Tapped="ThirdButton_Tapped">
<TextBlock x:Name="Icon3" />
<TextBlock x:Name="Text3" />
</StackPanel>
I chose stackpanel because that was the best way for me to have a bottom and horizontal aligned bar with an icon on top and text displaying under it.
(if you got a better idea feel free to post it)
Now the question is how i should write my C# code so that the Tapped event will load a second page into the viewport.
(further, the style I'm aiming for is something similar to what deezer uses, example is here: http://imgur.com/WNNi96v
Thank you in advance!
As #Pedro G. Dias says far better to use Button control as it has many more benefits (Visual States) that you associate with user clicking/ Tapping a Button rather than using the Tap event of StackPanel
You can customise a Button easily by utilising the ContentTemplate
<StackPanel Orientation="Horizontal" VerticalAlignment="Bottom">
<Button x:Name="FirstButton" Click="FirstButton_Click">
<Button.ContentTemplate>
<DataTemplate>
<StackPanel>
<TextBlock x:Name="Icon1" />
<TextBlock x:Name="Text1" />
</StackPanel>
</DataTemplate>
</Button.ContentTemplate>
</Button>
<!--More buttons here-->
</StackPanel>
Simply create buttons and place them inside each stackpanel. The button.Content property of the Button control will let you put the two textblocks inside it.

Find Index of Item in ItemsControl nested inside a LixtBox.Item

I have an ItemsControl nested inside a ListBox.ItemTemplate. The top ListBox is data-bound to an ObservableCollection. The collection is essentially a Purchase which contains Formulae which in turn contain individual products.
Now, if an individual product inside a formula is clicked, I would like it to be deleted from its formula. So, in the event handler, I should be able to determine the product's position using:
var index = [theItemsControl].ItemContainerGenerator.IndexFromContainer((sender as Button).TemplatedParent);
However, in my event handler, I do not how how to find the containing ItemsControl. I cannot give it a fixed name, since it is itself a part of a ListBox.ItemTemplate, meaning there will be multiple instances.
This is my XAML, stripped of any style-related stuff:
<ListBox x:Name="lbBasketFormulae" ItemsSource="{Binding Path=Purchase.Formulae}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Vertical">
<!-- Formula Title -->
<StackPanel Orientation="Horizontal">
<Label Width="30" Content="{Binding Path=Quantity}"></Label>
<Label Content="{Binding Path=FormulaType, Converter={StaticResource formulaTypeToNameConverter}}"></Label>
</StackPanel>
<!-- Formula Products -->
<ItemsControl ItemsSource="{Binding Path=Products}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button x:Name="bnBasketFormulaProduct" HorizontalContentAlignment="Left" Content="{Binding Path=Name}" Click="bnBasketFormulaProduct_Click">
<Button.Template>
<ControlTemplate TargetType="Button">
<TextBlock Text="{TemplateBinding Content}" />
</ControlTemplate>
</Button.Template>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
So, my question is: How do I find the position of my product inside my formula and the position of that formula inside the purchase, so that I can remove it from the ObservableCollection? (Changing the collection should then automatically reflect to the UI since it is data-bound.)
Many thanks in advance for any advice or hints!
Regards,
Chris
It is true that you should be able to do it via the viewmodel. But if you want you could use:
VisualTreeHelper.GetParent(myUserControl);
The answer is you don't find any positions of anything using ItemContainerGenerator.IndexFromContainer. Your problem is clearly taking place inside ViewModel and therefore you should seek for selected item or whatever inside your ViewModel and not in ListBox or code behind of Window.
Therefore I suggest you to create a proper ViewModel with proper Bindings.

Changing content according to selected option in WPF

I'm interested in creating an app that displays some buttons and changes a viewport according to the selected button. The viewport in my app is a ContentControl and I thought of changing its content whenever a button is clicked. However, I believe there's a better approach, by perhaps injecting the ViewModels of each of the Views I want to present to the ContentControl and styling them using DataTemplates (Since I want to avoid having a grid with many controls and just setting their Visibility property whenever I want to show a particular view). Which of the approaches seems better to you? Do you have a different approach for this?
The view should be something similar to this:
Thanks!
Usually have a ViewModel behind the window which contains:
ObservableCollection<IViewModel> AvailableViewModels
IViewModel SelectedViewModel
ICommand SetCurrentViewModelCommand
I display the AvailableViewModels using an ItemsControl, which has its ItemTemplate set to a Button. The Button.Command is bound to the SetCurrentViewModelCommand, and it passes the current data item from the AvailableViewModels collection in through the CommandParameter
To display the content area, I use a ContentControl with ContentControl.Content bound to SelectedViewModel, and DataTemplates get used to tell WPF how to render each ViewModel.
The end result is my XAML looks something like this:
<Window.Resources>
<DataTemplate DataType="{x:Type local:ViewModelA}">
<local:ViewA />
</DataTemplate>
<DataTemplate DataType="{x:Type local:ViewModelB}">
<local:ViewB />
</DataTemplate>
</Window.Resources>
<DockPanel>
<Border DockPanel.Dock="Left" BorderBrush="Black" BorderThickness="0,0,1,0">
<ItemsControl ItemsSource="{Binding AvailableViewModels}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Content="{Binding Name}"
Command="{Binding DataContext.SetCurrentViewModelCommand, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"
CommandParameter="{Binding }"
Margin="2,5"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Border>
<ContentControl Content="{Binding SelectedViewModel}" />
</DockPanel>
You can view an example of the full code used for such a setup on my blog

WPF Multiple ItemSources?

Is it possible to have multiple ItemSources for a single control?
Given the code below:
<ComboBox Margin="137,101,169,183" ItemsSource="{Binding collection}" SnapsToDevicePixels="True"
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<CheckBox Command="{Binding CheckCommand}" IsChecked="{Binding IsChecked}" Content="{Binding Name}"/>
<TextBlock Text="" />
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
The TextBlock within the ComboBox DataTemplate requires data from another property within the VM than that of the ComboBox. How can this be achieved?
Thanks.
You can use RelativeSource-FindAncestor to reach up the visual tree and grab a different DataContext.
For example (assuming the command is what you want):
Command=”{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ComboBox}}, Path=CheckCommand}”
This should also serve as a good resource.
Edit: Typo and resources.
If i remember correctly, DataTemplates run within their own scope and cannot directly use ElementNames defined outside the DataTemplate. You could however get around it by using StaticResource and referring to that directly from TextBlock inside the template.
I haven't tried Ragepotatos's approach to go outside DataTemplate scope but would love to know if that works out for you too.

Categories

Resources