Accessing this.content from ViewModel following MVVM design pattern - c#

I'm sure I'm missing something really silly and stupid here, and I'll probably kick myself when I see it, but I just have a simple question.
I've got some code in the constructor of the code-behind for a view with a grid, that does the following:
Grid mainGrid = this.Content as Grid;
MenuItem item = mainGrid.ContextMenu.Items[0] as MenuItem;
this.ApplySkinFromMenuItem(item);
So my question is how can I do this from the ViewModel? The ViewModel doesn't know what "this" is, and doesn't have a reference to "this" either.
It is my understanding that the view model object is created in XAML by calling:
<ObjectDataProvider x:Key="TimersHostViewModel" ObjectType="{x:Type local:TimersHostViewModel}"/>
And setting the data context like so:
<Grid DataContext="{StaticResource TimersHostViewModel}" Style="{DynamicResource styleBackground}">
But this doesn't give the TimersHostViewModel any knowledge about "this.Content", and saying TimersHost.Content doesn't help, because TimersHost isn't an actual object, but a class, and I need an actual object to get the ".Content" from, and it should be the right object, the object that is from the code behind, but how can I get that into the view model?
After all following MVVM means that the ViewModel shouldn't have any knowledge about the View, and the View shouldn't have any knowledge about the ViewModel, and they just communicate back and forth with bindings and INotifyPropertyChanged and other such message passing techniques. I've done a fair bit of this stuff in another application, so I'm some-what familiar with the basics, but still some-what new, and still learning and even re-learning.
I've included the full source below. As you can see I'm in the process of trying to get the code out of the code behind and into the ViewModel, but I'm running into a compiler error when attempting to get this.Content from the main grid.
XAML:
<Window
x:Class="TimersXP.TimersHost"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase"
xmlns:local="clr-namespace:TimersXP"
Name="TimersHostView"
SizeToContent="Height"
Title="TimersXP"
WindowStartupLocation="CenterScreen"
WindowStyle="ToolWindow">
<Window.Resources>
<ObjectDataProvider x:Key="TimersHostViewModel" ObjectType="{x:Type local:TimersHostViewModel}"/>
</Window.Resources>
<Grid.RowDefinitions>
<RowDefinition Height="21"/>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="90"/>
</Grid.ColumnDefinitions>
<!--Main Menu-->
<Menu IsMainMenu="True" Style="{DynamicResource styleBanner}" Margin="0,0,0,1">
<MenuItem Header="_Help" Style="{DynamicResource styleBanner}">
<MenuItem Header="_About" Style="{DynamicResource styleBanner}"/>
</MenuItem>
</Menu>
<!--Top Most Check Box-->
<CheckBox Content="Top Most" Grid.Column="1" Height="16" HorizontalAlignment="Left" Margin="11,2,0,0" Name="checkBox1" VerticalAlignment="Top" />
<!--Stopwatch & Countdown Tab Defintions-->
<TabControl Grid.Row="1" Grid.ColumnSpan="2" Style="{DynamicResource styleContentArea}">
<TabItem Header="Stopwatch"/>
<TabItem Header="Countdown"/>
</TabControl>
<!-- CONTEXT MENU -->
<Grid.ContextMenu>
<ContextMenu Style="{DynamicResource styleBanner}" MenuItem.Click="OnMenuItemClick">
<MenuItem Tag=".\Resources\Skins\BlackSkin.xaml" IsChecked="True">
<MenuItem.Header>
<Rectangle Width="120" Height="40" Fill="Black" />
</MenuItem.Header>
</MenuItem>
<MenuItem Tag=".\Resources\Skins\GreenSkin.xaml">
<MenuItem.Header>
<Rectangle Width="120" Height="40" Fill="Green" />
</MenuItem.Header>
</MenuItem>
<MenuItem Tag=".\Resources\Skins\BlueSkin.xaml">
<MenuItem.Header>
<Rectangle Width="120" Height="40" Fill="Blue" />
</MenuItem.Header>
</MenuItem>
</ContextMenu>
</Grid.ContextMenu>
</Grid>
</Window>
Code Behind:
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
namespace TimersXP
{
public partial class TimersHost : Window
{
public TimersHost()
{
try
{
InitializeComponent();
}
catch (Exception ex)
{
Debug.WriteLine("CTOR Exception: " + ex.Message);
}
// Load the default skin.
Grid mainGrid = this.Content as Grid;
MenuItem item = mainGrid.ContextMenu.Items[0] as MenuItem;
this.ApplySkinFromMenuItem(item);
}
public void OnMenuItemClick(object sender, RoutedEventArgs e)
{
MenuItem item = e.OriginalSource as MenuItem;
// Update the checked state of the menu items.
//Grid mainGrid = this.Content as Grid;
//foreach (MenuItem mi in mainGrid.ContextMenu.Items)
//mi.IsChecked = mi == item;
// Load the selected skin.
this.ApplySkinFromMenuItem(item);
}
void ApplySkinFromMenuItem(MenuItem item)
{
// Get a relative path to the ResourceDictionary which
// contains the selected skin.
string skinDictPath = item.Tag as string;
Uri skinDictUri = new Uri(skinDictPath, UriKind.Relative);
// Tell the Application to load the skin resources.
App app = Application.Current as App;
app.ApplySkin(skinDictUri);
}
}
}
ViewModel:
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
namespace TimersXP
{
public class TimersHostViewModel
{
public TimersHostViewModel()
{
// Load the default skin.
Grid mainGrid = this.Content as Grid; <---- ERROR HERE
}
//public void TimersHostViewModel()
//{
// // Load the default skin.
// Grid mainGrid = TimersHost.Content as Grid;
// MenuItem item = mainGrid.ContextMenu.Items[0] as MenuItem;
// //this.ApplySkinFromMenuItem(item);
//}
public void OnMenuItemClick(object sender, RoutedEventArgs e)
{
MenuItem item = e.OriginalSource as MenuItem;
// Update the checked state of the menu items.
//Grid mainGrid = this.Content as Grid;
//foreach (MenuItem mi in mainGrid.ContextMenu.Items)
// mi.IsChecked = mi == item;
// Load the selected skin.
this.ApplySkinFromMenuItem(item);
}
void ApplySkinFromMenuItem(MenuItem item)
{
// Get a relative path to the ResourceDictionary which contains the selected skin.
string skinDictPath = item.Tag as string;
Uri skinDictUri = new Uri(skinDictPath, UriKind.Relative);
// Tell the Application to load the skin resources.
App app = Application.Current as App;
app.ApplySkin(skinDictUri);
}
}
}

check out this link:
ContextMenu in MVVM
You need to bind your context menu items to a collection/property in your viewmodel. "This." will not work because that is the code behind and does not translate across to a view model.
Put this in view model:
class ContextItem : INotifyPropertyChanged
{
public string Name;
public ICommand Action;
public Brush Icon;
}
ObservableCollection<ContextItem> Items {get;set;}
then in your view's context menu:
<Grid.ContextMenu>
<ContextMenu ItemsSource="{Binding Items}/>
Anything you want to "pass" to the view needs to be a property/collection in your view model, you will never directly use a visual element object like a Gird/Context menu in you viewmodel. WPF handles the binding for you, which is the main benefit of WPF. Just make sure you implement INotifyPropertyChanged for the properties. I didn't to simplify the sample.
Now this does not mean there is never a case for code behind, but it should only involve visual elements, and not the data the visual elements bind to.
Hope this helps

Related

Adding a AnchorablesSource in AvalonDock using MVVM ViewModel first

I having problems adding an AvalonDock AnchorablesSource under a ViewModel first MVVM approach using Stylet.
My avalonDock XAML is as follows:
<DockingManager
Grid.Row="1"
DocumentsSource="{Binding Scl.Documents}"
AnchorablesSource="{Binding Scl.DocumentsAnchorable}"
x:Name="dockManager"
AllowMixedOrientation="True"
AutoWindowSizeWhenOpened="True"
IsVirtualizingAnchorable="True"
IsVirtualizingDocument="True"
ActiveContent="{Binding Scl.ActiveDocument, Mode=TwoWay}"
DocumentClosed="{s:Action DocumentClosed}"
>
<DockingManager.LayoutItemContainerStyle>
<Style TargetType="{x:Type LayoutItem}">
<Setter Property="Title" Value="{Binding Model.Title}"/>
<Setter Property="Height" Value="Auto"/>
<Setter Property="IconSource" Value="{Binding Model.IconSource}" />
</Style>
</DockingManager.LayoutItemContainerStyle>
<DockingManager.LayoutItemTemplate>
<DataTemplate>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<ContentControl s:View.Model="{Binding Content , FallbackValue=#ERROR Content.title#}"></ContentControl>
</Grid>
</DataTemplate>
</DockingManager.LayoutItemTemplate>
<LayoutRoot x:Name="root">
<LayoutPanel Orientation="Horizontal">
<LayoutAnchorablePane x:Name="LayoutAnchorablePane" DockWidth="50">
</LayoutAnchorablePane>
<LayoutDocumentPaneGroup>
<LayoutDocumentPane x:Name="LayoutDocumentPane">
<!-- This is where the new windows are typically added -->
</LayoutDocumentPane>
</LayoutDocumentPaneGroup>
<LayoutAnchorablePaneGroup DockWidth="250">
<LayoutAnchorablePane x:Name="LayoutAnchorablePane1">
</LayoutAnchorablePane>
</LayoutAnchorablePaneGroup>
</LayoutPanel>
<LayoutRoot.LeftSide>
<LayoutAnchorSide>
<LayoutAnchorGroup>
</LayoutAnchorGroup>
</LayoutAnchorSide>
</LayoutRoot.LeftSide>
</LayoutRoot>
</DockingManager>
In my ViewModel I have:
public ObservableCollection<LayoutDocument> Documents {get; set;} = new ObservableCollection<LayoutDocument>();
public ObservableCollection<LayoutAnchorable> DocumentsAnchorable { get; set; } = new ObservableCollection<LayoutAnchorable>();
When I add a new normal layout to the LayoutDocumentPane as follows it works perfectly:
public void NewLayout(Screen viewModel, string title)
{
if (IsOpen(title) == true)
{
return;
}
LayoutDocument layout = new LayoutDocument
{
Title = title,
Content = viewModel
};
Documents.Add(layout);
Documents.Move(Documents.Count - 1, 0);
ActiveDocument = layout;
}
But If try and add a new Anchorable Layout (even with a know working ViewModel like this I get an error.
public void NewLayoutAnchorable(Screen viewModel, string title)
{
if (IsOpen(title) == true)
{
return;
}
LayoutAnchorable layout = new LayoutAnchorable
{
Title = title,
Content = viewModel
};
DocumentsAnchorable.Add(layout);
//DocumentsAnchorable.Move(Documents.Count - 1, 0);
//ActiveDocument = layout;
}
The error I get is:
Exception thrown: 'Stylet.StyletViewLocationException' in Stylet.dll
Exception thrown: 'System.Windows.Markup.XamlParseException' in
PresentationFramework.dll Unable to transform ViewModel name
AvalonDock.Layout.LayoutAnchorable into a suitable View name
Does anyone know why Stylet can find the relevant View then the ViewModel is used for a LayoutDocument but not a LayoutAnchorable in AvalonDock?
Edit 1:
The issue does not appear to be with the LayoutAnchorable viewmodels, as I can add them to the ObservableCollection<LayoutDocument> and they find a view just fine.
It is only a problem if I try and add a viewmodel to the ObservableCollection<LayoutAnchorable> that I get the error, so it appear to be an issue with my AvalonDock XAML.
Edit 2:
If I remove the following line from my XAML:
<ContentControl s:View.Model="{Binding Content , FallbackValue=#ERROR Content.title#}"></ContentControl>
I can add ViewModels to both by observable collections, and the windows appear in both the LayoutPane and the AchorablePane, except the windows are missing their content.
It therefore appears that I need a LayoutItemTemplate that works for anchorable windows but I can't seem to find an example.
Edit 3:
I've made a bare bones project that demonstrates the problem here if anybody wants to have a play.
https://github.com/montyjohn/StyletAvalonDockTest.git
If Stylet and AvalonDock can be made to play nicely together it would be a great starting point so new applications.
I think there is a problem with DockingManager, because it sets different DataContexts for its items. The DataContext for ContentControl is LayoutDocument for DocumentPane but it is ContentPresenter (Parent of LayoutAnchorable) for AnchorablePane.
You can use this workaround. I added a template selector and modified ShellView.xaml file. Now it works.
ShellView.xaml
<Window x:Class="StyletAvalonDockTest.Views.ShellView"
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"
xmlns:ts="clr-namespace:StyletAvalonDockTest.TemplateSelectors"
xmlns:s="https://github.com/canton7/Stylet"
mc:Ignorable="d"
Title="ShellView" Height="450" Width="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Horizontal">
<Button Width="auto" Command="{s:Action NewLayout}">Add New Window</Button>
<Button Width="auto" Command="{s:Action NewLayoutAnchorable}">Add New Anchorable Pane</Button>
</StackPanel>
<DockingManager
Grid.Row="1"
DocumentsSource="{Binding Documents}"
AnchorablesSource="{Binding DocumentsAnchorable}"
>
<!--If the theme is removed, neither the new Layout, nor the New Anchorable Layout work-->
<DockingManager.Theme>
<Vs2013LightTheme />
</DockingManager.Theme>
<!--This adds the title to the new windows-->
<DockingManager.LayoutItemContainerStyle>
<Style TargetType="{x:Type LayoutItem}">
<Setter Property="Title" Value="{Binding Model.Title}"/>
</Style>
</DockingManager.LayoutItemContainerStyle>
<DockingManager.LayoutItemTemplateSelector>
<ts:PanesTemplateSelector>
<ts:PanesTemplateSelector.DocumentPaneTemplate>
<DataTemplate>
<ContentControl s:View.Model="{Binding Content}"/>
</DataTemplate>
</ts:PanesTemplateSelector.DocumentPaneTemplate>
<ts:PanesTemplateSelector.AnchoroblePaneTemplate>
<DataTemplate>
<ContentControl s:View.Model="{Binding Content.Content}"/>
</DataTemplate>
</ts:PanesTemplateSelector.AnchoroblePaneTemplate>
</ts:PanesTemplateSelector>
</DockingManager.LayoutItemTemplateSelector>
<LayoutRoot>
<LayoutPanel>
<LayoutAnchorablePane>
<!-- This is where the new Anchorable windows are added -->
</LayoutAnchorablePane>
<LayoutDocumentPaneGroup>
<LayoutDocumentPane>
<!-- This is where the new windows are added -->
</LayoutDocumentPane>
</LayoutDocumentPaneGroup>
</LayoutPanel>
</LayoutRoot>
</DockingManager>
</Grid>
PanesTemplateSelector.cs
using AvalonDock.Layout;
using System.Windows;
using System.Windows.Controls;
namespace StyletAvalonDockTest.TemplateSelectors
{
public class PanesTemplateSelector : DataTemplateSelector
{
public DataTemplate DocumentPaneTemplate { get; set; }
public DataTemplate AnchoroblePaneTemplate { get; set; }
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
if (item is LayoutDocument)
return DocumentPaneTemplate;
else
return AnchoroblePaneTemplate;
}
}
}

Implement data virtualisation in a UWP ListView without duplicating items

I have a large ListView which is largely made InkCanvas objects, it turns out that ListView implements data virtualisation to "cleverly" unload and load items in the view depending on the visible items in the view. The problem with this is that many times the ListView caches items and when a new item is added it essentially copy items already added in the view. So in my case, if the user adds a stroke to an Inkcanvas and then adds a new InkCanvas to the ListView, the new canvas contains the strokes from the previous canvas. As reported here this is because of the data virtualisation. My ListView is implemented as follows:
<Grid HorizontalAlignment="Stretch">
<ListView x:Name="CanvasListView" IsTapEnabled="False"
IsItemClickEnabled="False"
ScrollViewer.ZoomMode="Enabled"
ScrollViewer.HorizontalScrollMode="Enabled"
ScrollViewer.VerticalScrollMode="Enabled"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
ScrollViewer.VerticalScrollBarVisibility="Visible"
HorizontalAlignment="Stretch">
<!-- Make sure that items are not clickable and centered-->
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="HorizontalContentAlignment" Value="Center"/>
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel>
<local:CanvasControl Margin="0 2"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
MinWidth="1000" MinHeight="100" MaxHeight="400"
Background="LightGreen"/>
<Grid HorizontalAlignment="Stretch" Background="Black" Height="2"></Grid>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Stretch">
<InkToolbar x:Name="inkToolbar"
VerticalAlignment="Top"
Background="LightCoral"/>
<StackPanel HorizontalAlignment="Right">
<Button x:Name="AddButton" Content="Add Page" Click="Button_Click"/>
<TextBlock x:Name="PageCountText" />
</StackPanel>
</StackPanel>
</Grid>
A full example can be found here and here is a video of the issue.
Indeed if I turn off data virtualisation (or switch to an ItemsControl) everything works brilliantly. The problem however is that with a very large list, this approach has a heavy impact on performance (with 60+ InkCanvas controls the app just crashes). So is there a way to retain data virtualisation while avoiding the duplication of items? I have tried with VirtualizationMode.Standard but items are still duplicated.
To solve this problem, we must first understand why this problem occurs.
ListView has a reuse container inside, it will not endlessly create new list items, but will recycle.
In most cases, such recycling is not a problem. But it's special for InkCanvas.
InkCanvas is a stateful control. When you draw on InkCanvas, the handwriting is retained and displayed on the UI.
If your control is a TextBlock, this problem does not occur, because we can directly bind the value to TextBlock.Text, but for the Stroke of InkCanvas, we cannot directly bind, which will cause the so-called residue.
So in order to avoid this, we need to clear the state, that is, every time the InkCanvas is created or reloaded, the strokes in the InkCanvas are re-rendered.
1. Create a list for saving stroke information in ViewModel
public class ViewModel : INotifyPropertyChanged
{
// ... other code
public List<InkStroke> Strokes { get; set; }
public ViewModel()
{
Strokes = new List<InkStroke>();
}
}
2. Change the internal structure of CanvasControl
xaml
<Grid>
<InkCanvas x:Name="inkCanvas"
Margin="0 2"
MinWidth="1000"
MinHeight="300"
HorizontalAlignment="Stretch" >
</InkCanvas>
</Grid>
xaml.cs
public sealed partial class CanvasControl : UserControl
{
public CanvasControl()
{
this.InitializeComponent();
// Set supported inking device types.
inkCanvas.InkPresenter.InputDeviceTypes =
Windows.UI.Core.CoreInputDeviceTypes.Mouse |
Windows.UI.Core.CoreInputDeviceTypes.Pen;
}
private void StrokesCollected(InkPresenter sender, InkStrokesCollectedEventArgs args)
{
if (Data != null)
{
var strokes = inkCanvas.InkPresenter.StrokeContainer.GetStrokes().ToList();
Data.Strokes = strokes.Select(p => p.Clone()).ToList();
}
}
public ViewModel Data
{
get { return (ViewModel)GetValue(DataProperty); }
set { SetValue(DataProperty, value); }
}
public static readonly DependencyProperty DataProperty =
DependencyProperty.Register("Data", typeof(ViewModel), typeof(CanvasControl), new PropertyMetadata(null,new PropertyChangedCallback(Data_Changed)));
private static void Data_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if(e.NewValue!=null && e.NewValue is ViewModel vm)
{
var strokes = vm.Strokes.Select(p=>p.Clone());
var instance = d as CanvasControl;
instance.inkCanvas.InkPresenter.StrokesCollected -= instance.StrokesCollected;
instance.inkCanvas.InkPresenter.StrokeContainer.Clear();
try
{
instance.inkCanvas.InkPresenter.StrokeContainer.AddStrokes(strokes);
}
catch (Exception)
{
}
instance.inkCanvas.InkPresenter.StrokesCollected += instance.StrokesCollected;
}
}
}
In this way, we can keep our entries stable.

GridView, ItemTemplate, DataTemplate binding in C# code behind

I have following working XAML and C# code behind:
<Grid x:Name="MainGrid" Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<GridView ItemsSource="{Binding}">
<GridView.ItemTemplate>
<DataTemplate>
<Grid Height="100" Width="150">
<Grid.Background>
<SolidColorBrush Color="{Binding Color}"/>
</Grid.Background>
<StackPanel>
<StackPanel.Background>
<SolidColorBrush Color="{Binding Color}"/>
</StackPanel.Background>
<TextBlock FontSize="15" Margin="10" Text="{Binding Name}"/>
</StackPanel>
</Grid>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
</Grid>
CODE behind:
public MainPage()
{
this.InitializeComponent();
var _Colors = typeof(Colors)
.GetRuntimeProperties()
.Select(x => new
{
Color = (Color)x.GetValue(null),
Name = x.Name
});
this.DataContext = _Colors;
}
This works fine.
But I want to do all the XAML part in C# code behind. In XAML, only MainGrid will be there, all its child elements and bindings needs to be done in code behind.
I have tried something like this in MainPage_Loaded event:
private void MainPage_Loaded(object sender, RoutedEventArgs e)
{
try
{
GridView gridView = new GridView()
{
ItemTemplate = new DataTemplate
{
//Don't know what to add here
}
};
Grid grid = new Grid();
Binding bindingObject = new Binding();
bindingObject.Source = this;
grid.SetBinding(Grid.BackgroundProperty, bindingObject);
//...
// Don't know how to add grid inside gridView in Code.
//...
MainGrid.Children.Add(gridView);
}
catch (Exception ex)
{
}
}
First of all I'd like to advise you not to create elements in code unless you have a real good reason to do so.
Regardless, considering that item templates are factory-like objects for controls (you 'spawn' a new set of controls for each item). You use the FrameworkElementFactory to model the subtree and then assign that the item template's VisualTree property.

Detect binding property changed within a user control

I am working on a Windows Store app in which I have a user control as a data template inside flipview.
User Control: (ImagePage.xaml)
<UserControl
x:Name="userControl"
x:Class="MWC_online.Classes.ImagePage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:MWC_online.Classes"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="768"
d:DesignWidth="1366">
<Grid Background="#FFF0F0F0" Margin="4,0">
...
<Image Source="{Binding Img}" Stretch="None" HorizontalAlignment="Left" VerticalAlignment="Top" />
<StackPanel x:Name="stackPanel" HorizontalAlignment="Left" Margin="984,83,0,0" Width="325">
<Grid Background="{Binding Colour}">
<TextBlock Margin="30,30,30,15" Text="{Binding TextContent1}" FontWeight="Light" TextWrapping="Wrap" Foreground="#FF00ABE8" FontSize="29" />
</Grid>
<Grid Background="{Binding Colour}">
<TextBlock Margin="30,10,30,30" Text="{Binding TextContent2}" TextWrapping="Wrap" Foreground="#FF606060" FontSize="17" />
</Grid>
</StackPanel>
</Grid>
</UserControl>
User Control Class: (ImagePage.xaml.cs)
private static void OnTitleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e){
StackPanel stackPanel = (StackPanel)d;
stackPanel.Visibility = Visibility.Collapsed;
}
public string TextContent1
{
get { return (string)GetValue(TextContent1Property); }
set { SetValue(TextContent1Property, value); }
}
public static readonly DependencyProperty TextContent1Property =
DependencyProperty.Register("TextContent1", typeof(string), typeof(ImagePage), new PropertyMetadata("", new PropertyChangedCallback(OnTextContent1Changed)));
private static void OnTextContent1Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
// what I want to do is if TextContent1 or TextContent2 has no value
// turn the stackpanel visibility to collapsed
StackPanel stackPanel = (StackPanel)d;
stackPanel.Visibility = Visibility.Collapsed;
}
Everything is working fine EXCEPT the OnTextContent1Changed is not firing! so I dont know if this is the right way of doing things but basically I just want to switch an UI element within the user control ON or OFF depending on the data binding that is being fed into it.
The TextBlock doesn't have a DataContext to find the DependencyProperty on. If you give your Grid a name in ImagePage.xaml:
<Grid Background="#FFF0F0F0" Margin="4,0" x:Name="MyGrid">
Then you can set its DataContext in the ImagePage constructor in ImagePage.xaml.cs:
public ImagePage()
{
InitializeComponent();
MyGrid.DataContext = this;
}
Which tells the Grid (and its decendents) to look for Dependency Properties on the ImagePage class. With this, the Dependency Property should get bound correctly. Another problem, though, is that you're telling the DependencyProperty that it is on an ImagePage with typeof(ImagePage), but then casting it to a StackPanel, which will fail every time:
StackPanel stackPanel = (StackPanel)d; // Throws System.InvalidCastException
You could fix this by giving a name to the StackPanel and referencing it directly in your .cs file.

wpf context menu left-click

Is it possible to attach context menu to a wpf control and have it opened on left-click (as opposed to more customary right-click)? I want to achieve that using xaml only (this should be a part of my control's view template).
Here is a way to show context menu on left-click:
Create a new left button handler on the Border element:
<Border x:Name="Win"
Width="40"
Height="40"
Background="Purple"
MouseLeftButtonUp="UIElement_OnMouseLeftButtonUp">
and then add this:
private void UIElement_OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
e.Handled = true;
var mouseDownEvent =
new MouseButtonEventArgs(Mouse.PrimaryDevice,
Environment.TickCount,
MouseButton.Right)
{
RoutedEvent = Mouse.MouseUpEvent,
Source = Win,
};
InputManager.Current.ProcessInput(mouseDownEvent);
}
What it does, it basically maps the left-click into right-click. For reusability, you can wrap this into an attached behavior.
Here is how I would do a simple example of what I am suggesting:
The XAML:
<Window x:Class="LeftClickMenu.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow">
<Grid>
<Border Width="400" Height="300" Background="#ccc" BorderBrush="#333"
BorderThickness="1"
MouseLeftButtonDown="Border_MouseLeftButtonDown"
MouseRightButtonUp="Border_MouseRightButtonUp">
<Border.ContextMenu>
<ContextMenu x:Name="myContextMenu">
<MenuItem Header="Menu Item 1" />
<MenuItem Header="Menu Item 2" />
<MenuItem Header="Menu Item 3" />
<MenuItem Header="Menu Item 4" />
<MenuItem Header="Menu Item 5" />
</ContextMenu>
</Border.ContextMenu>
</Border>
</Grid>
</Window>
And the code-behind:
using System.Windows;
using System.Windows.Input;
namespace LeftClickMenu
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void Border_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
myContextMenu.IsOpen = true;
}
private void Border_MouseRightButtonUp(object sender, MouseButtonEventArgs e)
{
e.Handled = true;
}
}
}
I also added the extra MouseRightButtonUp event to inhibit the right-click popup of the context menu.
Create a method to programmatically open a submenu as stated in this SO article:
Show menu programmatically in WPF
Create an event for LeftMouseButtonDown and call that event in XAML.

Categories

Resources