WPF and MDI model - c#

After many years of Windows Forms development, i decided to experiment with WPF. In the company i work for, i have built a great amount of software based on the MDI style and i would like to continue to do so when using WPF.
I know that MDI is not "supported" my WPF and i am trying to find a work around this matter. I also know that i can mimic MDI behavior by the WPF tab control but this is not an optimal solution for me.
Users here, are used to have software with an MDI form and many forms underneath that serve as monitoring different tasks, asynchronously, and being always visible.
Is there any way i can achieve this functionality in WPF without Tab or using 3rd party controls and without some kine of WinForms interop ?

One option is to use the following project:
http://wpfmdi.codeplex.com/
Other option is to make something yourself, which allows for interesting stuff relating to multimonitor behaviour etc..

I have built MDI for WPF using functionality of Popup control. I suggest you do not use common win forms way which based on separate controls, but use MVVM for WPF.
So, each MDI window is Popup which wraps a view model. Application itself is base window which hosts view models and operates with windows via view models.
I provide a real life MDI window sample. Be aware that this is not complete sample, but it is enought to demonstrate some basic conceptions. All necessary classes are custom, so no dependency from the third part components:
<DataTemplate DataType="{x:Type vm:Pane}">
<DataTemplate.Resources>
<ControlTemplate x:Key="uiFreePaneTemplate" TargetType="ContentControl">
<Popup
x:Name="PART_DRAG" HorizontalOffset="{Binding X}" VerticalOffset="{Binding Y}"
IsOpen="{Binding IsOpened}" VerticalAlignment="Top" HorizontalAlignment="Left"
AllowsTransparency="True">
<Border
Width="{Binding Width}" Height="{Binding Height}"
BorderThickness="2,2,2,2" Margin="0" CornerRadius="5"
BorderBrush="{StaticResource WindowBackgroundBrush}"
Background="{StaticResource WindowBackgroundBrush}">
<ContentControl Template="{StaticResource Resizer}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!--Pane header-->
<Thumb Grid.Row="0" Width="Auto" Height="21">
<i:Interaction.Triggers>
<i:EventTrigger EventName="DragDelta">
<mvvm:EventToCommand Command="{Binding DragCommand}" PassEventArgsToCommand="True" />
</i:EventTrigger>
</i:Interaction.Triggers>
<Thumb.Template>
<ControlTemplate>
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Grid.Background>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="#FF0C286C" Offset="1"/>
<GradientStop Color="Transparent"/>
</LinearGradientBrush>
</Grid.Background>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Label Content="{Binding Name}" Grid.Column="0" />
<Button Template="{StaticResource CloseButton}"
Grid.Column="1"
Margin="1,1,3,1" />
</Grid>
</ControlTemplate>
</Thumb.Template>
</Thumb>
<Grid Grid.Row="1" Background="{StaticResource ControlBackgroundBrush}">
<!--Pane content-->
<AdornerDecorator>
<ContentPresenter Content="{TemplateBinding Content}" />
</AdornerDecorator>
<ResizeGrip x:Name="WindowResizeGrip"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
IsTabStop="false"/>
</Grid>
</Grid>
</ContentControl>
</Border>
</Popup>
</ControlTemplate>
</DataTemplate.Resources>
<ContentControl x:Name="uiBorder" Content="{Binding Model}" />
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding IsFree}" Value="True">
<Setter TargetName="uiBorder" Property="ContentControl.Template" Value="{StaticResource uiFreePaneTemplate}" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
View model:
public class Pane : HideableChildViewModel, IPane
{
private IViewModel _model;
private bool _isFree = true;
private string _name;
private double _coordinateX;
private double _coordinateY;
private double _width = 200.0;
private double _height = 400.0;
private ICommand _closeCommand;
private ICommand _dragCommand;
private ICommand _resizeCommand;
/// <summary>
/// Initializes a new instance of the Pane class.
/// </summary>
/// <param name="parent">The parent view model</param>
/// <param name="parentPropertySelector">Selector of the parent property</param>
/// <param name="model">VM to place within the pane</param>
public Pane(
IViewModel parent,
Expression<Func<object>> parentPropertySelector,
IViewModel model)
: base(parent, parentPropertySelector)
{
this.Model = model;
this._dragCommand = new DragPaneCommand();
this._resizeCommand = new ResizeCommand();
if (model != null && model is ICloseableVM)
{
this._closeCommand = new ClosePaneCommand();
}
else
{
this._closeCommand = new HideCommand();
}
}
#region Properties
/// <summary>
/// Gets or sets VM to place within the pane
/// </summary>
public IViewModel Model
{
get
{
return this._model;
}
set
{
if (this._model != value)
{
this._model = value;
this.RaisePropertyChanged(() => this.Model);
}
}
}
/// <summary>
/// Gets or sets name of the pane
/// </summary>
[LayoutSettings(IsKey = true)]
public string Name
{
get
{
return this._name;
}
set
{
if (this._name != value)
{
this._name = value;
this.RaisePropertyChanged(() => this.Name);
}
}
}
/// <summary>
/// Gets or sets X coordinate
/// </summary>
[LayoutSettings]
public double X
{
get
{
return this._coordinateX;
}
set
{
if (this._coordinateX != value)
{
this._coordinateX = value;
this.RaisePropertyChanged(() => this.X);
}
}
}
/// <summary>
/// Gets or sets Y coordinate
/// </summary>
[LayoutSettings]
public double Y
{
get
{
return this._coordinateY;
}
set
{
if (this._coordinateY != value)
{
this._coordinateY = value;
this.RaisePropertyChanged(() => this.Y);
}
}
}
/// <summary>
/// Gets or sets width
/// </summary>
[LayoutSettings]
public double Width
{
get
{
return this._width;
}
set
{
if (this._width != value)
{
this._width = value;
this.RaisePropertyChanged(() => this.Width);
}
}
}
/// <summary>
/// Gets or sets height
/// </summary>
[LayoutSettings]
public double Height
{
get
{
return this._height;
}
set
{
if (this._height != value)
{
this._height = value;
this.RaisePropertyChanged(() => this.Height);
}
}
}
/// <summary>
/// Gets or sets a value indicating whether pane is free
/// </summary>
public bool IsFree
{
get
{
return this._isFree;
}
set
{
if (this._isFree != value)
{
this._isFree = value;
this.OnIsFreeChanged(this._isFree);
this.RaisePropertyChanged(() => this.IsFree);
}
}
}
#endregion
#region Commands
/// <summary>
/// Gets command for pane closing
/// </summary>
public ICommand CloseCommand
{
get { return this._closeCommand; }
}
/// <summary>
/// Gets command for pane dragging
/// </summary>
public ICommand DragCommand
{
get { return this._dragCommand; }
}
/// <summary>
/// Gets command for pane resize
/// </summary>
public ICommand ResizeCommand
{
get { return this._resizeCommand; }
}
#endregion
private void OnIsFreeChanged(bool isFree)
{
if (!isFree)
{
return;
}
IDockContainer oContainer = ((IChildViewModel)((IChildViewModel)this.Parent).Parent).Parent as IDockContainer;
if (oContainer != null)
{
this.SetParent(oContainer, () => oContainer.FreeItems);
}
}
}

Related

User Control Dependency Property Updating Multiple control values

I have a feeling my base issue with this problem is binding some internal properties.
In the usercontrol I have a rectangle that contains a linear gradient. I created a dependency property to be able to give a value (0 to 1) to specify where the gradient line should be in the rectangle. To adjust this value dynamically I connected a slider to it for testing.
I also added some feedback textblocks to let me know how some of the data is flowing and if it is flowing. And from that I can tell my binding to my GradientStops.offset values from my user control properties are not working. How do I get the user control to update the rectangle gradient by just changing RectangleLevel.RectLevel value?
USERCONTROL XAML
<UserControl x:Class="RectDynamicGradient.RectangleLevel"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:RectDynamicGradient"
xmlns:diag="clr-namespace:System.Diagnostics;assembly=WindowsBase"
mc:Ignorable="d"
d:DesignHeight="180" d:DesignWidth="80">
<Grid>
<Rectangle HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Rectangle.Fill>
<LinearGradientBrush>
<GradientStop Color="Cyan" Offset="{Binding Gradient_top_color, diag:PresentationTraceSources.TraceLevel=High}"/>
<GradientStop Color="Black" Offset="{Binding Gradient_bottom_color, diag:PresentationTraceSources.TraceLevel=High}"/>
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
</Grid>
</UserControl>
USERCONTROL CODE
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace RectDynamicGradient
{
/// <summary>
/// Interaction logic for RectangleLevel.xaml
/// </summary>
public partial class RectangleLevel : UserControl
{
public double Gradient_bottom_color { get; set; }
public double Gradient_top_color { get; set; }
public double RectLevel
{
get { return (double)GetValue(RectLevelProperty); }
set { SetValue(RectLevelProperty, value); }
}
// Using a DependencyProperty as the backing store for RectLevel. This enables animation, styling, binding, etc...
public static readonly DependencyProperty RectLevelProperty =
DependencyProperty.Register("RectLevel", typeof(double), typeof(RectangleLevel), new FrameworkPropertyMetadata(0.0,
FrameworkPropertyMetadataOptions.AffectsRender,
new PropertyChangedCallback(ChangeLevel),
new CoerceValueCallback(CoerceLevel)),
new ValidateValueCallback(ValidateLevel));
public static void ChangeLevel(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
RectangleLevel t = d as RectangleLevel;
t.UpdateGradientStops((double)e.NewValue);
}
public static object CoerceLevel(DependencyObject d, object value)
{
if (value is string valstring)
{
if (Double.TryParse(valstring, out double lvl))
{
return lvl;
}
}
if (value is double valdouble)
{
return valdouble;
}
throw new Exception();
}
public static bool ValidateLevel(object value)
{
double? level = 0;
if (value is string valstring)
{
if (Double.TryParse(valstring, out double lvl))
{
level = lvl;
}
}
if (value is double valdouble)
{
level = valdouble;
}
if (level.HasValue && level >= 0 && level <= 1)
return true;
else
return false;
}
public RectangleLevel()
{
InitializeComponent();
this.DataContext = this;
}
private void UpdateGradientStops(double level)
{
double scale = 0;
if (level < .5)
{
scale = level;
}
else if (level >= .5)
{
scale = 1 - level;
}
Gradient_top_color = level;
Gradient_bottom_color = level + (level * .1 * scale);
}
}
}
MAINWINDOW XAML
<Window x:Class="RectDynamicGradient.MainWindow"
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:local="clr-namespace:RectDynamicGradient"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Slider x:Name="TheSlider" RenderTransformOrigin="0.5,0.5" VerticalAlignment="Center" LargeChange="0.1" Maximum="1" SmallChange="0.01" >
<Slider.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleY="2"/>
<SkewTransform/>
<RotateTransform/>
<TranslateTransform Y="2"/>
</TransformGroup>
</Slider.RenderTransform>
</Slider>
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" Grid.Column="0">
<TextBlock Text="SliderValue:"/>
<TextBlock HorizontalAlignment="Center" FontSize="32" Text="{Binding ElementName=TheSlider, Path=Value}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Grid.Column="1">
<TextBlock Text="UC LEVEL Value:"/>
<TextBlock Grid.Row="1" HorizontalAlignment="Center" FontSize="32" Text="{Binding ElementName=MyUserControl, Path=RectLevel}"/>
</StackPanel>
</Grid>
<StackPanel Orientation="Horizontal" Grid.Row="2" HorizontalAlignment="Center">
<local:RectangleLevel x:Name="MyUserControl" Width="80" VerticalAlignment="Stretch" RectLevel="{Binding ElementName=TheSlider, Path=Value}"/>
</StackPanel>
</Grid>
</Window>
MAINWINDOW CODE
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace RectDynamicGradient
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
}
EDIT
After feedback from the comments the improvments were made.
DataContext removed this and added relative source to user control (named "uc")
Changed NotifyProperties into DependencyProperties
Replaced DataContext
<GradientStop Color="Cyan" Offset="{Binding ElementName=uc, Path=GradientFilledColor}"/>
Updated Dependency Properties
/// <summary>
/// Property to change the GradientEmptyColor (GradientStop offset) of the rectangle by interfacing with the dependency property.
/// </summary>
public double GradientEmptyColor
{
get { return (double)GetValue(GradientEmptyColorProperty); }
set { SetValue(GradientEmptyColorProperty, value); }
}
/// <summary>
/// Using a DependencyProperty as the backing store for GradientEmptyColor. This enables animation, styling, binding, etc...
/// </summary>
public static readonly DependencyProperty GradientEmptyColorProperty =
DependencyProperty.Register("GradientEmptyColor", typeof(double), typeof(RectangleLevel), new PropertyMetadata(0.0));
/// <summary>
/// Property to change the GradientFilledColor (GradientStop offset) of the rectangle by interfacing with the dependency property.
/// </summary>
public double GradientFilledColor
{
get { return (double)GetValue(GradientFilledColorProperty); }
set { SetValue(GradientFilledColorProperty, value); }
}
/// <summary>
/// Using a DependencyProperty as the backing store for GradientFilledColor. This enables animation, styling, binding, etc...
/// </summary>
public static readonly DependencyProperty GradientFilledColorProperty =
DependencyProperty.Register("GradientFilledColor", typeof(double), typeof(RectangleLevel), new PropertyMetadata(0.0));
The DependencyProperty provides the same capability as the INotifyProperty PropertChanged event (uses a different mechanism though).
As mentioned by #Clemens setting datacontext to self is a terrible idea and would break linking when the user control is used through-out the project and datacontext is set to something else.
I am still open to suggestions that help support good practices and learning.
UPDATED CODE
USERCONTROL XAML
<UserControl x:Class="RectDynamicGradient.RectangleLevel"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:RectDynamicGradient"
mc:Ignorable="d"
d:DesignHeight="180" d:DesignWidth="80" x:Name="uc">
<Grid>
<Rectangle HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Rectangle.Fill>
<LinearGradientBrush StartPoint=".5,1" EndPoint=".5,0">
<GradientStop Color="Cyan" Offset="{Binding ElementName=uc, Path=GradientFilledColor}"/>
<GradientStop Color="Black" Offset="{Binding ElementName=uc, Path=GradientEmptyColor}"/>
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
</Grid>
</UserControl>
USERCONTROL CODE
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
namespace RectDynamicGradient
{
/// <summary>
/// Interaction logic for RectangleLevel.xaml
/// </summary>
public partial class RectangleLevel : UserControl
{
/// <summary>
/// Property to change the GradientEmptyColor (GradientStop offset) of the rectangle by interfacing with the dependency property.
/// </summary>
public double GradientEmptyColor
{
get { return (double)GetValue(GradientEmptyColorProperty); }
set { SetValue(GradientEmptyColorProperty, value); }
}
/// <summary>
/// Using a DependencyProperty as the backing store for GradientEmptyColor. This enables animation, styling, binding, etc...
/// </summary>
public static readonly DependencyProperty GradientEmptyColorProperty =
DependencyProperty.Register("GradientEmptyColor", typeof(double), typeof(RectangleLevel), new PropertyMetadata(0.0));
/// <summary>
/// Property to change the GradientFilledColor (GradientStop offset) of the rectangle by interfacing with the dependency property.
/// </summary>
public double GradientFilledColor
{
get { return (double)GetValue(GradientFilledColorProperty); }
set { SetValue(GradientFilledColorProperty, value); }
}
/// <summary>
/// Using a DependencyProperty as the backing store for GradientFilledColor. This enables animation, styling, binding, etc...
/// </summary>
public static readonly DependencyProperty GradientFilledColorProperty =
DependencyProperty.Register("GradientFilledColor", typeof(double), typeof(RectangleLevel), new PropertyMetadata(0.0));
/// <summary>
/// Property to change the level (GradientStop offsets) of the rectangle by interfacing with the dependency property.
/// </summary>
public double RectLevel
{
get { return (double)GetValue(RectLevelProperty); }
set { SetValue(RectLevelProperty, value); }
}
/// <summary>
/// Using a DependencyProperty as the backing store for RectLevel. This enables animation, styling, binding, etc...
/// </summary>
public static readonly DependencyProperty RectLevelProperty =
DependencyProperty.Register("RectLevel", typeof(double), typeof(RectangleLevel), new FrameworkPropertyMetadata(0.0,
FrameworkPropertyMetadataOptions.AffectsRender,
new PropertyChangedCallback(ChangeLevel),
new CoerceValueCallback(CoerceLevel)),
new ValidateValueCallback(ValidateLevel));
/// <summary>
/// PropertyChangedCallback for DependencyProperty RectLevelProperty.
/// </summary>
/// <param name="d">Dependency object causing the event.</param>
/// <param name="e">Change event arguments.</param>
public static void ChangeLevel(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
RectangleLevel t = d as RectangleLevel;
t.UpdateGradientStops((double)e.NewValue);
}
/// <summary>
/// CoerceValueCallback for DependencyProperty RectLevelProperty.
/// </summary>
/// <param name="d">Dependency object causing the event.</param>
/// <param name="value">Value being sent to RectLevelProperty be coerced.</param>
/// <returns></returns>
public static object CoerceLevel(DependencyObject d, object value)
{
if (value is string valstring)
{
if (Double.TryParse(valstring, out double lvl))
{
return lvl;
}
}
if (value is double valdouble)
{
return valdouble;
}
throw new Exception();
}
/// <summary>
/// ValidateValueCallback for DependencyProperty RectLevelProperty
/// </summary>
/// <param name="value">Value being sent to RectLevelProperty be validated.</param>
/// <returns>True, if valid value between and including 0 and 1.</returns>
public static bool ValidateLevel(object value)
{
double? level = 0;
if (value is string valstring)
{
if (Double.TryParse(valstring, out double lvl))
{
level = lvl;
}
}
if (value is double valdouble)
{
level = valdouble;
}
if (level.HasValue && level >= 0 && level <= 1)
return true;
else
return false;
}
/// <summary>
/// Constructor sets DataContext to itself.
/// </summary>
public RectangleLevel()
{
InitializeComponent();
this.DataContext = this;
}
/// <summary>
/// Updates the variables binded to the GradientStops for the rectangle.
/// </summary>
/// <param name="level">Level where the GradientStops should be. Valid value is 0 to 1 representing 0 to 100% filled.</param>
private void UpdateGradientStops(double level)
{
double scale = 0;
if (level < .5)
{
scale = level;
}
else if (level >= .5)
{
scale = 1 - level;
}
GradientFilledColor = level;
GradientEmptyColor = level + (level * .1 * scale);
}
}
}
MAINWINDOW XAML
<Window x:Class="RectDynamicGradient.MainWindow"
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:local="clr-namespace:RectDynamicGradient"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Slider x:Name="TheSlider" RenderTransformOrigin="0.5,0.5" VerticalAlignment="Center" LargeChange="0.1" Maximum="1" SmallChange="0.01" >
<Slider.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleY="2"/>
<SkewTransform/>
<RotateTransform/>
<TranslateTransform Y="2"/>
</TransformGroup>
</Slider.RenderTransform>
</Slider>
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" Grid.Column="0">
<TextBlock Text="SliderValue:"/>
<TextBlock HorizontalAlignment="Center" FontSize="32" Text="{Binding ElementName=TheSlider, Path=Value}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Grid.Column="1">
<TextBlock Text="UC LEVEL Value:"/>
<TextBlock Grid.Row="1" HorizontalAlignment="Center" FontSize="32" Text="{Binding ElementName=MyUserControl, Path=RectLevel}"/>
</StackPanel>
</Grid>
<StackPanel Orientation="Horizontal" Grid.Row="2" HorizontalAlignment="Center">
<local:RectangleLevel x:Name="MyUserControl" Width="80" VerticalAlignment="Stretch" RectLevel="{Binding ElementName=TheSlider, Path=Value}"/>
</StackPanel>
</Grid>
</Window>

WPF tree view visibility converters not updating when item is added to bound observable collection

I have built out a tree view that is bound to an observable collection and have built it with connecting lines between each tree view item. The view model being used implements INotifyPropertyChanged and I'm using PropertyChanged.Fody for weaving. The tree view is bound to the collection and is updating great EXCEPT for one thing. When I add a new item to the list during runtime, the UI doesn't seem to update properly. I have tried everything under the sun that I could find searching the web on how to force an update to the UI without having to send a command to rebuild the entire tree when I add a root item, which does work, but there has to be another way I'm not finding.
I am using Ninject for dependency injection.
I'll place all of the code below my question, for reference. Again, all of this is working great, until an item is added to the collection at runtime. The item is added and visible in the tree view once added to the collection, but the last line converter doesn't update all the graphics properly.
Considering the following image:
Once an item is added, the node that now becomes second to last, his connecting lines visibility does not update and he still thinks he's the last on the branch. I've tried all types of UI refresh methods I could find, nothing worked. I'm missing something here, but am fairly new to WPF. Any advice anyone can provide would be extremely appreciated. Thanks!
Here's how I'm initially building the tree view, which works great:
ProjectHelpers.JsonObject = JObject.Parse(File.ReadAllText(ProjectPath.BaseDataFullPath));
//-- Get the channels, which are the top level tree elements
var children = ProjectHelpers.GetChannels();
//-- add the channels to the application channel collection
IoC.Application.Channels = new ObservableCollection<ProjectTreeItemViewModel>();
foreach(var c in children)
IoC.Application.Channels.Add(new ProjectTreeItemViewModel(c.Path, ProjectItemType.Channel));
Which is contained within this class:
/// <summary>
/// The view model for the main project tree view
/// </summary>
public class ProjectTreeViewModel : BaseViewModel
{
/// <summary>
/// Name of the image displayed above the tree view UI
/// </summary>
public string RootImageName => "blink";
/// <summary>
/// Default constructor
/// </summary>
public ProjectTreeViewModel()
{
BuildProjectTree();
}
#region Handlers : Building project data tree
/// <summary>
/// Builds the entire project tree
/// </summary>
public void BuildProjectTree()
{
ProjectHelpers.JsonObject = JObject.Parse(File.ReadAllText(ProjectPath.BaseDataFullPath));
//-- Get the channels, which are the top level tree elements
var children = ProjectHelpers.GetChannels();
//-- add the channels to the application channel collection
IoC.Application.Channels = new ObservableCollection<ProjectTreeItemViewModel>();
foreach(var c in children)
IoC.Application.Channels.Add(new ProjectTreeItemViewModel(c.Path, ProjectItemType.Channel));
}
#endregion
}
The view model for the items that get added to the observable collection
/// <summary>
/// The view model that represents an item within the tree view
/// </summary>
public class ProjectTreeItemViewModel : BaseViewModel
{
/// <summary>
/// Default constructor
/// </summary>
/// <param name="path">The JSONPath for the item</param>
/// <param name="type">The type of project item type</param>
public ProjectTreeItemViewModel(string path = "", ProjectItemType type = ProjectItemType.Channel)
{
//-- Create commands
ExpandCommand = new RelayCommand(Expand);
GetNodeDataCommand = new RelayCommand(GetNodeData);
FullPath = path;
Type = type;
//-- Setup the children as needed
ClearChildren();
}
#region Public Properties
/// <summary>
/// The JSONPath for this item
/// </summary>
public string FullPath { get; set; }
/// <summary>
/// The type of project item
/// </summary>
public ProjectItemType Type { get; set; }
/// <summary>
/// Gets and sets the image name associated with project tree view headers.
/// </summary>
public string ImageName
{
get
{
switch (Type)
{
case ProjectItemType.Channel:
return "channel";
case ProjectItemType.Device:
return "device";
default:
return "blink";
}
}
}
/// <summary>
/// Gets the name of the item as a string
/// </summary>
public string Name => ProjectHelpers.GetPropertyValue(FullPath, "Name");
/// <summary>
/// Gets the associated driver as a string
/// </summary>
public string Driver => ProjectHelpers.GetPropertyValue(FullPath, "Driver");
/// <summary>
/// A list of all children contained inside this item
/// </summary>
public ObservableCollection<ProjectTreeItemViewModel> Children { get; set; }
/// <summary>
/// Indicates if this item can be expanded
/// </summary>
public bool CanExpand => (Type != ProjectItemType.Device);
/// <summary>
/// Indicates that the tree view item is selected, bound to the UI
/// </summary>
public bool IsSelected { get; set; }
/// <summary>
/// Indicates if the current item is expanded or not
/// </summary>
public bool IsExpanded
{
get {
return (Children?.Count(f => f != null) >= 1);
}
set {
//-- If the UI tells us to expand...
if (value == true)
//-- Find all children
Expand();
//-- If the UI tells us to close
else
this.ClearChildren();
}
}
#endregion
#region Commands
/// <summary>
/// The command to expand this item
/// </summary>
public ICommand ExpandCommand { get; set; }
/// <summary>
/// Command bound by left mouse click on tree view item
/// </summary>
public ICommand GetNodeDataCommand { get; set; }
#endregion
#region Public Methods
/// <summary>
/// Expands a tree view item
/// </summary>
public void Expand()
{
//-- return if we are either a device or already expanded
if (this.Type == ProjectItemType.Device || this.IsExpanded == true)
return;
//-- find all children
var children = ProjectHelpers.GetChildrenByName(FullPath, "Devices");
this.Children = new ObservableCollection<ProjectTreeItemViewModel>(
children.Select(c => new ProjectTreeItemViewModel(c.Path, ProjectHelpers.GetItemType(FullPath))));
}
/// <summary>
/// Clears all children of this node
/// </summary>
public void ClearChildren()
{
//-- Clear items
this.Children = new ObservableCollection<ProjectTreeItemViewModel>();
//-- Show the expand arrow if we are not a device
if (this.Type != ProjectItemType.Device)
this.Children.Add(null);
}
/// <summary>
/// Clears the children and expands it if it has children
/// </summary>
public void Reset()
{
this.ClearChildren();
if (this.Children?.Count > 0)
this.Expand();
}
#endregion
#region Public Methods
/// <summary>
/// Shows the view model data in the node context data grid
/// </summary>
public void GetNodeData()
{
switch (Type)
{
//-- get the devices associated with that channel
case ProjectItemType.Channel:
IoC.Application.UpdateDeviceDataContext(FullPath);
break;
//-- get the tags associated with that device
case ProjectItemType.Device:
IoC.Application.UpdateTagDataContext(FullPath);
break;
}
}
#endregion
}
Here's my template for the tree view item:
<Style x:Key="BaseTreeViewItemTemplate" TargetType="{x:Type TreeViewItem}">
<Setter Property="Panel.ZIndex" Value="{Binding RelativeSource={RelativeSource Self}, Converter={StaticResource TreeViewItemZIndexConverter}}" />
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="Black" />
<Setter Property="Padding" Value="1,2,2,2"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TreeViewItem}">
<Grid Name="ItemRoot">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="20"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid Name="Lines" Grid.Column="0" Grid.Row="0">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<!-- L shape -->
<Border Grid.Row="0" Grid.Column="1" Name="TargetLine" BorderThickness="1 0 0 1" SnapsToDevicePixels="True" BorderBrush="Red"/>
<!-- line that follows a tree view item -->
<Border Name="LineToNextItem"
Visibility="{Binding RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource TreeLineVisibilityConverter}}"
Grid.Row="1" Grid.Column="1" BorderThickness="1 0 0 0" SnapsToDevicePixels="True" BorderBrush="Blue"/>
</Grid>
<ToggleButton x:Name="Expander" Grid.Column="0" Grid.Row="0"
Style="{StaticResource ExpandCollapseToggleStyle}"
IsChecked="{Binding Path=IsExpanded, RelativeSource={RelativeSource TemplatedParent}}"
ClickMode="Press"/>
<!-- selected border background -->
<Border Name="ContentBorder" Grid.Column="1" Grid.Row="0"
HorizontalAlignment="Left"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Padding="{TemplateBinding Padding}"
SnapsToDevicePixels="True">
<ContentPresenter x:Name="ContentHeader" ContentSource="Header" MinWidth="20"/>
</Border>
<Grid Grid.Column="0" Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Border BorderThickness="1 0 0 0"
Name="TargetBorder"
Grid.Column="1"
SnapsToDevicePixels="True"
BorderBrush="Olive"
Visibility="{Binding ElementName=LineToNextItem, Path=Visibility}"
/>
</Grid>
<ItemsPresenter x:Name="ItemsHost" Grid.Column="1" Grid.Row="1" />
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="HasItems" Value="false">
<Setter TargetName="Expander" Property="Visibility" Value="Hidden"/>
</Trigger>
<Trigger Property="IsExpanded" Value="false">
<Setter TargetName="ItemsHost" Property="Visibility" Value="Collapsed"/>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="HasHeader" Value="False"/>
<Condition Property="Width" Value="Auto"/>
</MultiTrigger.Conditions>
<Setter TargetName="ContentHeader" Property="MinWidth" Value="75"/>
</MultiTrigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="HasHeader" Value="False"/>
<Condition Property="Height" Value="Auto"/>
</MultiTrigger.Conditions>
<Setter TargetName="ContentHeader" Property="MinHeight" Value="19"/>
</MultiTrigger>
<Trigger Property="IsEnabled" Value="True">
<Setter Property="Foreground" Value="{StaticResource OffWhiteBaseBrush}"/>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsSelected" Value="True"/>
<Condition Property="IsSelectionActive" Value="True"/>
</MultiTrigger.Conditions>
<Setter TargetName="ContentBorder" Property="Background" Value="{StaticResource SelectedTreeViewItemColor}"/>
<Setter Property="Foreground" Value="White" />
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
My custom tree view control
<UserControl ...>
<UserControl.Template>
<ControlTemplate TargetType="UserControl">
<StackPanel Background="Transparent"
Margin="8"
Orientation="Vertical"
VerticalAlignment="Top"
HorizontalAlignment="Left"
TextBlock.TextAlignment="Left">
<Image x:Name="Root"
ContextMenuOpening="OnContextMenuOpened"
Width="18" Height="18"
HorizontalAlignment="Left"
RenderOptions.BitmapScalingMode="HighQuality"
Margin="2.7 0 0 3"
Source="{Binding RootImageName, Converter={x:Static local:HeaderToImageConverter.Instance}}" />
<TreeView Name="ProjectTreeView"
Loaded="OnTreeViewLoaded"
SelectedItemChanged="OnTreeViewSelectedItemChanged"
ContextMenuOpening="OnContextMenuOpened"
BorderBrush="Transparent"
Background="Transparent"
VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingStackPanel.VirtualizationMode="Recycling"
Style="{StaticResource ResourceKey=BaseTreeViewTemplate}"
ItemContainerStyle="{StaticResource ResourceKey=BaseTreeViewItemTemplate}"
ItemsSource="{Binding ApplicationViewModel.Channels, Source={x:Static local:ViewModelLocator.Instance}}">
<TreeView.ContextMenu>
<ContextMenu>
<MenuItem Header="New Item" />
<MenuItem Header="Cut" />
<MenuItem Header="Copy" />
<MenuItem Header="Delete" />
<MenuItem Header="Diagnostics" />
<MenuItem Header="Properties" />
</ContextMenu>
</TreeView.ContextMenu>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Path=Children}">
<StackPanel Orientation="Horizontal" Margin="2">
<Image Width="15" Height="15" RenderOptions.BitmapScalingMode="HighQuality"
Margin="-1 0 0 0"
Source="{Binding Path=ImageName, Converter={x:Static local:HeaderToImageConverter.Instance}}" />
<TextBlock Margin="6,2,2,0" VerticalAlignment="Center" Text="{Binding Path=Name}" />
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
<ContentPresenter />
</StackPanel>
</ControlTemplate>
</UserControl.Template>
</UserControl>
The visibility converter for the connection lines in the tree view template
/// <summary>
/// Visibility converter for a connecting line inside the tree view UI
/// </summary>
public class TreeLineVisibilityConverter : BaseValueConverter<TreeLineVisibilityConverter>
{
public override object Convert(object value, Type targetType = null, object parameter = null, CultureInfo culture = null)
{
TreeViewItem item = (TreeViewItem)value;
ItemsControl ic = ItemsControl.ItemsControlFromItemContainer(item);
bool isLastItem = (ic.ItemContainerGenerator.IndexFromContainer(item) == ic.Items.Count - 1);
return isLastItem ? Visibility.Hidden : Visibility.Visible;
}
public override object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
The problem exists due to this binding:
Visibility="{Binding RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource TreeLineVisibilityConverter}}"
You are binding to the the item container itself. This value never changes, therefore the Binding is only triggered once when the template is being applied to the container.
You should bind to a property that changes too, whenever the ItemsSource changes. I think the best solution is one that moves this logic to the items and/or the converter.
For this purpose I've added an IsLast property to the data model ProjectTreeItemViewModel which must raise INotifyPropertyChanged.PropertyChanged on changes.
The initial default value of this property should be false.
The border visibility binds to this property using your existing, but modified TreeLineVisibilityConverter.
The converter has to be turned into a IMultiValueConverter as we need to bind to the new ProjectTreeItemViewModel.IsLast and to the item itself using a MultiBinding.
Whenever a new item is added to the TreeView, its template will be loaded. This will trigger the MultiBinding and therefore the IMultiValueConverter. The converter checks if the current item is the last. If so, he will
Set the previous item ProjectTreeItemViewModel.IsLast to false, which will re-trigger
the MultiBinding for the previous item to show the line.
Set the the current ProjectTreeItemViewModel.IsLast to true.
Return the appropriate Visibility.
TreeLineVisibilityConverter.cs
public class TreeLineVisibilityConverter : IMultiValueConverter
{
public override object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
TreeViewItem item = (TreeViewItem) values[0];
ItemsControl ic = ItemsControl.ItemsControlFromItemContainer(item);
int lastIndex = ic.Items.Count - 1;
bool isLastItem = (ic.ItemContainerGenerator.IndexFromContainer(item) == lastIndex);
if (isLastItem)
{
ResetIsLastOfPrevousItem(ic.Items.Cast<ProjectTreeItemViewModel>(), lastIndex);
(item.DataContext as ProjectTreeItemViewModel).IsLast = true;
}
return isLastItem
? Visibility.Hidden
: Visibility.Visible;
}
private void ConvertBack(IEnumerable<ProjectTreeItemViewModel> items, int lastIndex)
{
ProjectTreeItemViewModel previousItem = items.ElementAt(lastIndex - 1);
if (previousItem.IsLast && items.Count() > 1)
{
previousItem.IsLast = false;
}
}
public override object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
ControlTemplate of TreeViewItem
<ControlTemplate TargetType="TreeViewItem">
...
<!-- line that follows a tree view item -->
<Border Name="LineToNextItem">
<Border.Visibility>
<MultiBinding Converter="{StaticResource TreeLineVisibilityConverter}">
<Binding RelativeSource="{RelativeSource TemplatedParent}"/>
<Binding Path="IsLast" />
</MultiBinding>
</Border.Visibility>
</Border>
...
</ControlTemplate>
Remarks
For performance reasons you should consider to add a Parent property to your ProjectTreeItemViewModel. It's more efficient to traverse the model tree, than traversing the visual tree. Then in your ControlTemplate you simply replace the binding to the TemplatedParent (TreeViewItem) with a binding to the DataContext of the ControlTemplate e.g., {Binding} (or <Binding /> in case of the MultiBinding), which will return the current ProjectTreeItemViewModel. From here you can check if it's the last by accessing the ProjectTreeItemViewModel.Children property via the ProjectTreeItemViewModel.Parent. This way you don't have use the ItemContainerGenerator and don't have to cast the items of the ItemsControl.Items to IEnumerable<ProjectTreeItemViewModel>.
MVVM tree view example
This is a simple example on how to build a tree using MVVM. This example pretends to create the data tree from a text file.
See the ProjectTreeItem class to see how to traverse the tree using recursion e.g. GetTreeRoot().
At the end is also a revised version of the TreeLineVisibilityConverter to show how you can get to the parent collection using the Parent reference (and therefore without any need of static properties).
ProjectTreeItem.cs
// The data view model of the tree items.
// Since this is the binding source of the TreeView,
// this class should implement INotifyPropertyChanged.
// This classes property setters are simplified.
public class ProjectTreeItem : INotifyPropertyChanged
{
/// <summary>
/// Default constructor
/// </summary>
public ProjectTreeItem(string data)
{
this.Data = data;
this.Parent = null;
this.Children = new ObservableCollection<ProjectTreeItem>();
}
// Traverse tree and expand subtree.
public ExpandChildren()
{
foreach (var child in this.Children)
{
child.IsExpanded = true;
child.ExpandChildren();
}
}
// Traverse complete tree and expand each item.
public ExpandTree()
{
// Get the root of the tree
ProjectTreeItem rootItem = GetTreeRoot(this);
foreach (var child in rootItem.Children)
{
child.IsExpanded = true;
child.ExpandChildren();
}
}
// Traverse the tree to the root using recursion.
private ProjectTreeItem GetTreeRoot(ProjectTreeItem treeItem)
{
// Check if item is the root
if (treeItem.Parent == null)
{
return treeItem;
}
return GetTreeRoot(treeItem.Parent);
}
public string Data { get; set; }
public bool IsExpanded { get; set; }
public ProjectTreeItem Parent { get; set; }
public ObservableCollection<ProjectTreeItem> Children { get; set; }
}
Repository.cs
// A model class in the sense of MVVM
public class Repository
{
public ProjectTreeItem ReadData()
{
var lines = File.ReadAllLines("/path/to/data");
// Create the tree structure from the file data
return CreateDataModel(lines);
}
private ProjectTreeItem CreateDataModel(string[] lines)
{
var rootItem = new ProjectTreeItem(string.Empty);
// Pretend each line contains tokens separated by a whitespace,
// then each line is a parent and the tokens its children.
// Just to show how to build the tree by setting Parent and Children.
foreach (string line in lines)
{
rootItem.Children.Add(CreateNode(line));
}
return rootItem;
}
private ProjectTreeItem CreateNode(string line)
{
var nodeItem = new ProjectTreeItem(line);
foreach (string token in line.Split(' '))
{
nodeItem.Children.Add(new ProjectTreeItem(token) {Parent = nodeItem});
}
return nodeItem;
}
}
DataController.cs
// Another model class in the sense of MVVM
public class DataController
{
public DataController()
{
// Create the model. Alternatively use constructor
this.Repository = new Repository();
}
public IEnumerable<ProjectTreeItem> GetData()
{
return this.Repository.ReadData().Children;
}
private Repository Repository { get; set; }
}
MainViewModel.cs
// The data view model of the tree items.
// Since this is a binding source of the view,
// this class should implement INotifyPropertyChanged.
// This classes property setters are simplified.
public class MainViewModel : INotifyPropertyChanged
{
public MainViewModel()
{
// Create the model. Alternatively use constructor injection.
this.DataController = new DataController();
Initialize();
}
private void Initialize()
{
IEnumerable<ProjectTreeItem> treeData = this.DataController.GetData();
this.TreeData = new ObservableCollection<ProjectTreeItem>(treeData);
}
public ObservableCollection<ProjectTreeItem> TreeData { get; set; }
private DataController DataController { get; set; }
}
TreeLineVisibilityConverter.cs
public class TreeLineVisibilityConverter : IMultiValueConverter
{
public override object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
ProjectTreeItem item = values[0] as ProjectTreeItem;
// If current item is root return
if (item.Parent == null)
{
return Binding.DoNothing;
}
ProjectTreeItem parent = item?.Parent ?? item;
int lastIndex = item.Parent.Chilidren.Count - 1;
bool isLastItem = item.Parent.Chilidren.IndexOf(item) == lastIndex);
if (isLastItem)
{
ResetIsLastOfPrevousItem(item.Parent.Chilidren, lastIndex);
item.IsLast = true;
}
return isLastItem
? Visibility.Hidden
: Visibility.Visible;
}
private void ConvertBack(IEnumerable<ProjectTreeItem> items, int lastIndex)
{
ProjectTreeItem previousItem = items.ElementAt(lastIndex - 1);
if (previousItem.IsLast && items.Count() > 1)
{
previousItem.IsLast = false;
}
}
public override object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
UserControl.xaml
<UserControl>
<UserControl.DataContext>
<MainViewModel />
<UserControl.DataContext>
<UserControl.Resources>
<ControlTemplate TargetType="TreeViewItem">
...
<!-- line that follows a tree view item -->
<Border Name="LineToNextItem">
<Border.Visibility>
<MultiBinding Converter="{StaticResource TreeLineVisibilityConverter}">
<Binding />
<Binding Path="IsLast" />
</MultiBinding>
</Border.Visibility>
</Border>
...
</ControlTemplate>
<UserControl.Resources>
<TreeView ItemsSource="{Binding TreeData}" />
</UserControl>
Thanks to #BionicCode for the help here, means a lot. I wanted to share my implementation of view model traversal in lieu of visual tree traversal. I ended up not creating a field to reference the parent container in the ProjectTreeItemViewModel class, but instead created a ParentIndex and a ChildIndex that allows me to quickly access the item I need by referencing the FullPath property, which is just the JSONPath to the json content. Honestly, I'm not quite sure how you meant to include a reference to the parent container in the class, but would like to see your suggested implementation. Thanks again, #BionicCode, have a good weekend!
Here's my the converter now:
/// <summary>
/// Visibility converter for the connecting lines on the tree view UI
/// </summary>
public class ConnectingLineVisibilityConverter : IMultiValueConverter
{
/// <summary>
/// Returns the proper visibility according to location on the tree view UI
/// </summary>
public object Convert(object[] values, Type targetType = null, object parameter = null, CultureInfo culture = null)
{
ProjectTreeItemViewModel viewModel = (ProjectTreeItemViewModel)values[0];
//-- collection context by default is the channels
var collection = IoC.Application.Channels;
int currentIndex = viewModel.ParentIndex;
if (viewModel.Type == ProjectItemType.Device) {
//-- change the collection context to the children of this channel
collection = collection[currentIndex].Children;
currentIndex = viewModel.ChildIndex;
}
int lastIndex = collection.Count - 1;
bool isLastItem = (currentIndex == lastIndex);
//-- is it the last of it's branch?
if (isLastItem) {
ResetPreviousSibling(collection, lastIndex);
viewModel.IsLast = true;
}
return isLastItem ? Visibility.Hidden : Visibility.Visible;
}
/// <summary>
/// Resets the previous sibling IsLast flag once a new item is added to the collection
/// </summary>
/// <param name="collection">The collection to search</param>
/// <param name="lastIndex">The index of the previous sibling</param>
private void ResetPreviousSibling(ObservableCollection<ProjectTreeItemViewModel> collection, int lastIndex)
{
//-- there's only one item in the collection
if (lastIndex == 0)
return;
//-- get the previous sibling and reset it's IsLast flag, if necessary
ProjectTreeItemViewModel previousSibling = collection[lastIndex - 1];
if (previousSibling.IsLast)
previousSibling.IsLast = false;
}
public object[] ConvertBack(object value, Type[] targetTypes = null, object parameter = null, CultureInfo culture = null)
{
throw new NotImplementedException();
}
}
Then, the binding becomes...
<!-- connecting line to the next item -->
<Border Name="LineToNextItem" Grid.Row="1" Grid.Column="1" BorderThickness="1 0 0 0" SnapsToDevicePixels="True" BorderBrush="Blue">
<Border.Visibility>
<MultiBinding Converter="{StaticResource ConnectingLineVisibilityConverter}">
<Binding />
<Binding Path="IsLast" />
</MultiBinding>
</Border.Visibility>
</Border>

Custom UserControl events

I have an UserControl defined as follows:
<UserControl x:Class="Speaker.View.Controls.Prompt"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:conv="clr-namespace:Speaker.View.Converters"
mc:Ignorable="d" Height="Auto" Width="300" x:Name="PromptBox">
<UserControl.Resources>
<conv:VisibilityConverter x:Key="VConverter" />
</UserControl.Resources>
<Border Background="White" Padding="10" BorderThickness="1"
BorderBrush="Gray" CornerRadius="10" Height="80"
Visibility="{Binding Path=Show, ElementName=PromptBox,
Converter={StaticResource VConverter}}"
UseLayoutRounding="True">
<Border.Effect>
<DropShadowEffect BlurRadius="20" RenderingBias="Quality" />
</Border.Effect>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="20" />
<RowDefinition Height="10" />
<RowDefinition Height="20" />
</Grid.RowDefinitions>
<TextBox x:Name="InputText" Width="Auto" Height="20"
Text="{Binding Path=InfoText, ElementName=PromptBox, Mode=OneWay}"
Grid.Row="0" BorderThickness="0" Foreground="#FF8D8D8D"
GotFocus="InputText_GotFocus" LostFocus="InputText_LostFocus" />
<Separator Grid.Row="1" />
<Button Content="{Binding Path=ButtonText, ElementName=PromptBox}" Grid.Row="2"
Width="100" Command="{Binding Path=OkCommand, ElementName=PromptBox}" />
</Grid>
</Border>
What I want to do is this:
when the user clicks on the button, I'd like to run some code (obviously :) ) - this control will be used in some other controls / windows, and the code I'd like to run will be different depending on a scenarion. So how do I bind the Command property of this button with some custom command? Example usage:
<ctrls:Prompt Show="{Binding ShouldLogIn}" ButtonText="{Binding LogInText}"
InfoText="{Binding LogInInfo}" OkCommand="what goes here???" Grid.Row="0" Grid.ZIndex="2" />
Also - I follow the MVVM patern, using the MVVMLight fw, so I'd like the solution to follow it as well.
So the question is - How do I bind to the Button.Command from outside of the prompt control?
I would also suggest making a CustomControl, but if you want to use your UserControl you will need to add a DependencyProperty in your code behind.
public partial class Prompt : UserControl
{
private bool _canExecute;
private EventHandler _canExecuteChanged;
/// <summary>
/// DependencyProperty for the OKCommand property.
/// </summary>
public static readonly DependencyProperty OKCommandProperty = DependencyProperty.Register("OKCommand", typeof(ICommand), typeof(Prompt), new PropertyMetadata(OnOKCommandChanged));
/// <summary>
/// Gets or sets the command to invoke when the OKButton is pressed.
/// </summary>
public ICommand OKCommand
{
get { return (ICommand)GetValue(OKCommandProperty); }
set { SetValue(OKCommandProperty, value); }
}
/// <summary>
/// Gets a value that becomes the return value of
/// System.Windows.UIElement.IsEnabled in derived classes.
/// </summary>
protected override bool IsEnabledCore
{
get { return base.IsEnabledCore && _canExecute; }
}
// Command dependency property change callback.
private static void OnOKCommandChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
Prompt p = (Prompt)d;
p.HookUpCommand((ICommand)e.OldValue, (ICommand)e.NewValue);
}
public Prompt()
{
InitializeComponent();
}
// Add the command.
private void AddCommand(ICommand command)
{
EventHandler handler = new EventHandler(CanExecuteChanged);
_canExecuteChanged = handler;
if (command != null)
command.CanExecuteChanged += _canExecuteChanged;
}
private void CanExecuteChanged(object sender, EventArgs e)
{
if (OKCommand != null)
_canExecute = OKCommand.CanExecute(null);
CoerceValue(UIElement.IsEnabledProperty);
}
// Add a new command to the Command Property.
private void HookUpCommand(ICommand oldCommand, ICommand newCommand)
{
// If oldCommand is not null, then we need to remove the handlers.
if (oldCommand != null)
RemoveCommand(oldCommand);
AddCommand(newCommand);
}
// Remove an old command from the Command Property.
private void RemoveCommand(ICommand command)
{
EventHandler handler = CanExecuteChanged;
command.CanExecuteChanged -= handler;
}
}

WPF ComboBox selection change on TabItem selection change

I have a combobox in a tab item in MVVM. This tab can be created multiple times in my application (same view, same view model but different instance), so I can switch from one tab to another (but they are tab of the same type).
It works perfectly with every WPF control, but with combobox I have a strange behaviour:
the focus tab, when it loses focus, gets the selected item of the combox box of the tab that the application is focusing on.
If I switch from 2 tabs that are not of the same type everything works correctly, any idea about that? Thanks.
XAML:
<CollectionViewSource x:Key="StatusView" Source="{Binding Path=StatusList}"/>
<ComboBox Name="_spl2Status" Grid.Column="3" Grid.Row="0"
ItemsSource="{Binding Source={StaticResource StatusView}}"
SelectedValue="{Binding Path=CurrentSPL2.ID_SPL2_STATUS, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
SelectedValuePath="FL_TYPE"
DisplayMemberPath="ID_TYPE">
</ComboBox>
VM:
public List<NullableByteEnumType> StatusList
{
get
{
return (SPC_SPL2.SPL2StatusCollection.Skip(1)).ToList();
}
}
private SPC_SPL2 _currentSPL2 = null;
public SPC_SPL2 CurrentSPL2
{
get
{
if (_currentSPL2== null)
Controller.Execute(delegate(IResult result)
{
Dictionary<string, object> parameters = new Dictionary<string, object>();
parameters.Add("FL_ACTIVE", true);
parameters.Add("ID_SPL2", _itemcode);
Model.Invalidate(typeof(SPC_SPL2), Filter.GENERIC<SPC_SPL2>(parameters, "ID_SPL2"));
Model.Include<SPC_SPL2>();
if (Model.Appendload(result) == false)
return false;
Debug.Assert(Context.SPC_SPL2.Count == 1);
_currentSPL2= Context.SPC_SPL2.FirstOrDefault();
return result.Successful;
});
return _currentSPL2;
}
set
{
_currentSPL2= value;
OnPropertyChanged(() => CurrentSPL2);
}
}
my tabs are handled in this way:
<Grid>
<Border Grid.Row="0">
<ContentControl
Content="{Binding Path=Workspaces}"
ContentTemplate="{StaticResource MasterWorkspacesTemplate}"
/>
</Border>
</Grid>
where
<DataTemplate x:Key="MasterWorkspacesTemplate">
<TabControl IsSynchronizedWithCurrentItem="True"
BorderThickness="0"
ItemsSource="{Binding}"
SelectedItem="{Binding}"
ItemContainerStyleSelector="{StaticResource TabItemTemplate}"
/>
</DataTemplate>
and workspaces (my viewmodels list) (T is a class who inherit from viewModelBase)
public T CurrentWorkspace
{
get { return WorkspacesView.CurrentItem as T; }
}
private ObservableCollection<T> _workspaces;
public ObservableCollection<T> Workspaces
{
get
{
if (_workspaces == null)
{
_workspaces = new ObservableCollection<T>();
_workspaces.CollectionChanged += _OnWorkspacesChanged;
}
return _workspaces;
}
}
protected ICollectionView WorkspacesView
{
get
{
ICollectionView collectionView = CollectionViewSource.GetDefaultView(Workspaces);
Debug.Assert(collectionView != null);
return collectionView;
}
}
I have recreated your problem. But I couldn't find any issue. Please look at the code below and you might get the solustion. Here is my solution.
MyTab view model
public class MyTab : ViewModelBase
{
#region Declarations
private ObservableCollection<string> statusList;
private string selectedStatus;
#endregion
#region Properties
/// <summary>
/// Gets or sets the header.
/// </summary>
/// <value>The header.</value>
public string Header { get; set; }
/// <summary>
/// Gets or sets the content.
/// </summary>
/// <value>The content.</value>
public string Content { get; set; }
/// <summary>
/// Gets or sets the status list.
/// </summary>
/// <value>The status list.</value>
public ObservableCollection<string> StatusList
{
get
{
return statusList;
}
set
{
statusList = value;
NotifyPropertyChanged("StatusList");
}
}
/// <summary>
/// Gets or sets the selected status.
/// </summary>
/// <value>The selected status.</value>
public string SelectedStatus
{
get
{
return selectedStatus;
}
set
{
selectedStatus = value;
NotifyPropertyChanged("SelectedStatus");
}
}
#endregion
}
MainViewModel view model
public class MainViewModel : ViewModelBase
{
#region Declarations
private ObservableCollection<MyTab> tabs;
private MyTab selectedTab;
#endregion
#region Properties
/// <summary>
/// Gets or sets the tabs.
/// </summary>
/// <value>The tabs.</value>
public ObservableCollection<MyTab> Tabs
{
get
{
return tabs;
}
set
{
tabs = value;
NotifyPropertyChanged("Tabs");
}
}
/// <summary>
/// Gets or sets the selected tab.
/// </summary>
/// <value>The selected tab.</value>
public MyTab SelectedTab
{
get
{
return selectedTab;
}
set
{
selectedTab = value;
NotifyPropertyChanged("SelectedTab");
}
}
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="MainViewModel"/> class.
/// </summary>
public MainViewModel()
{
this.Tabs = new ObservableCollection<MyTab>();
MyTab tab1 = new MyTab();
tab1.Header = "tab1";
tab1.Content = "Tab 1 content";
ObservableCollection<string> tab1StatusList = new ObservableCollection<string>();
tab1StatusList.Add("tab1 item1");
tab1StatusList.Add("tab1 item2");
tab1StatusList.Add("tab1 item3");
tab1.StatusList = tab1StatusList;
tab1.SelectedStatus = tab1StatusList.First();
this.Tabs.Add(tab1);
MyTab tab2 = new MyTab();
tab2.Header = "tab2";
tab2.Content = "Tab 2 content";
ObservableCollection<string> tab2StatusList = new ObservableCollection<string>();
tab2StatusList.Add("tab2 item1");
tab2StatusList.Add("tab2 item2");
tab2StatusList.Add("tab2 item3");
tab2.StatusList = tab2StatusList;
tab2.SelectedStatus = tab2StatusList.First();
this.Tabs.Add(tab2);
this.SelectedTab = tab1;
}
#endregion
}
And finally this is my XAML
<Window x:Class="ComboboxSelectedItem.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:viewModel="clr-namespace:ComboboxSelectedItem.ViewModels"
Title="MainWindow" Height="350" Width="525">
<Grid Name="mainGrid">
<Grid.DataContext>
<viewModel:MainViewModel />
</Grid.DataContext>
<TabControl
ItemsSource="{Binding Tabs, Mode=TwoWay}"
SelectedItem="{Binding SelectedTab}">
<TabControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Header}" Margin="0 0 20 0"/>
</StackPanel>
</DataTemplate>
</TabControl.ItemTemplate>
<!--Content section-->
<TabControl.ContentTemplate>
<DataTemplate>
<Grid>
<StackPanel Orientation="Vertical">
<TextBlock
Text="{Binding Content}" />
<ComboBox
ItemsSource="{Binding StatusList}"
SelectedItem="{Binding SelectedStatus}" />
</StackPanel>
</Grid>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</Grid>
</Window>
Are you absolutely sure that you are creating a new instance of the viewmodel. If not, then the comboboxes are sharing the same collectionviewsource which means that a change in one combobox will be reflected in all comboboxes. I had this same problem myself.
Try declaring the collection view source in code:
CollectionViewSource StatusListViewSource = new CollectionViewSource();
StatusListViewSource.Source = SPL2StatusCollection;
then in xaml change binding to the collectionviewsource:
ItemsSource="{Binding StatusListViewSource.View}"
I converted from vb so it might need some edits.
Does that help?

XAML not compiling when trying to bind commands to TextBox

I have a WPF application (same app I have written about earlier) where I'm using the event handlers shown in Samuel Jack's page at http://blog.functionalfun.net/2008/09/hooking-up-commands-to-events-in-wpf.html. I am trying to add an event handler to the TextBox in my UI to do the same action which happens when a button is pressed. However, it won't compile, the compilation error is this:
Error 2 A 'Binding' cannot be used within a 'TextBox' collection. A 'Binding' can
only be set on a DependencyProperty of a DependencyObject.
Here is the XAML (note, I want the same behaviour to happen where pressing Enter while the TextBox is in focus has the same action as clicking on the button:
Here is the View Model code:
/// <summary>
/// The ViewModel in the MVVM pattern
/// </summary>
public class ApplicationViewModel : INotifyPropertyChanged
{
#region Private members
private string searchString;
private string emailString;
private string toolName;
private NexusApp selectedApp;
private ICommand searchButtonCmd;
private ICommand clearButtonCmd;
private ICommand emailButtonCmd;
private ICommand textboxKeyCmd;
#endregion
#region INotifyPropertyChanged implementation
/// <summary>
/// Notifies the view that the observablecollection bound to the listview has changed
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(string propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
#region Properties
/// <summary>
/// Collection bound to listview in ui view
/// </summary>
public ObservableCollection<NexusApp> AppCollection { get; set; }
/// <summary>
/// The application record selected by user
/// </summary>
public NexusApp SelectedApp
{
get
{
return this.selectedApp;
}
set
{
this.selectedApp = value;
this.emailString = this.selectedApp.Email;
this.toolName = this.selectedApp.Name;
}
}
/// <summary>
/// String entered by user into search box
/// </summary>
public string AppToSearch
{
get
{
return searchString;
}
set
{
searchString = value;
}
}
/// <summary>
/// Email address of team that owns app user is trying to request
/// </summary>
public string AppToRequest
{
get
{
return emailString;
}
set
{
emailString = value;
}
}
#endregion
/// <summary>
/// Default constructor, initialises and populates collection
/// </summary>
public ApplicationViewModel()
{
this.AppCollection = ApplicationsModel.Current;
}
#region Button command handlers
/// <summary>
/// Handles search button
/// </summary>
public ICommand SearchButtonPressed
{
get
{
if (this.searchButtonCmd == null)
{
this.searchButtonCmd = new RelayCommand(SearchButtonPressedExecute, c => CanSearch);
}
return this.searchButtonCmd;
}
}
/// <summary>
/// Handles clear button
/// </summary>
public ICommand ClearButtonPressed
{
get
{
if (this.clearButtonCmd == null)
{
this.clearButtonCmd = new RelayCommand(ClearButtonPressedExecute, c => CanClear);
}
return this.clearButtonCmd;
}
}
/// <summary>
/// Handles Request Application button
/// </summary>
public ICommand EmailButtonPressed
{
get
{
if (this.emailButtonCmd == null)
{
this.emailButtonCmd = new RelayCommand(EmailButtonPressedExecute, c => CanEmail);
}
return this.emailButtonCmd;
}
}
public ICommand TextBoxKeyPressed
{
get
{
if (this.textboxKeyCmd == null)
{
this.textboxKeyCmd = new RelayCommand(TextBoxKeyPressedExecute, c => CanSearch);
}
return this.textboxKeyCmd;
}
}
private void SearchButtonPressedExecute(object parameter)
{
try
{
ApplicationsModel.Current.Search(this.searchString);
}
catch (Exception e)
{
string error = String.Format("Could not refresh repository list: {0}", e.Message);
MessageBox.Show(error, "Error Performing Search", MessageBoxButton.OK, MessageBoxImage.Error);
}
NotifyPropertyChanged();
}
private void ClearButtonPressedExecute(object parameter)
{
try
{
this.searchString = String.Empty;
this.AppToSearch = String.Empty;
this.AppToSearch = String.Empty;
ApplicationsModel.Current.ClearSearch();
NotifyPropertyChanged();
}
catch (Exception e)
{
string error = String.Format("Could not refresh repository list: {0}", e.Message);
MessageBox.Show(error, "Error Clearing Search", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void EmailButtonPressedExecute(object parameter)
{
if (String.IsNullOrEmpty(this.toolName))
{
MessageBox.Show("Please select one of the applications to request", "Please select application", MessageBoxButton.OK, MessageBoxImage.Hand);
return;
}
try
{
string message;
if (EmailHelper.IsValidEmail(this.emailString))
{
message = String.Format("Request for application {0} successfully sent", this.toolName);
}
else
{
message = String.Format("Could not send request for access\nThe email address is not defined for tool {0}.\nThe Equity Nexus team has been alerted, asking them to investigate", this.toolName);
}
ApplicationsModel.Current.Email(this.emailString, this.toolName);
MessageBox.Show(message, "Request Sent", MessageBoxButton.OK, MessageBoxImage.Information);
}
catch (Exception e)
{
string error = String.Format("Could not send application request email, error: {0}", e.Message);
MessageBox.Show(error, "Erorr sending request", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void TextBoxKeyPressedExecute(object parameter)
{
MessageBox.Show("Hit textbox event execute");
}
/// <summary>
/// Ability to do search, always true
/// </summary>
public bool CanSearch
{ get { return true; } }
/// <summary>
/// Ability to clear search, always true
/// </summary>
public bool CanClear
{ get { return true; } }
/// <summary>
/// Ability to send email, always true
/// </summary>
public bool CanEmail
{ get { return true; } }
#endregion
}
Here is the RelayCommand code:
/// <summary>
/// Hooks up commands to the actions in Button Behaviour class
/// </summary>
public class RelayCommand : ICommand
{
readonly Action<object> _execute;
readonly Predicate<object> _canExecute;
public RelayCommand(Action<object> execute)
: this(execute, null)
{
}
public RelayCommand(Action<object> execute, Predicate<object> canExecute)
{
if (execute == null)
{
throw new ArgumentNullException("execute");
}
_execute = execute;
_canExecute = canExecute;
}
public bool CanExecute(object parameter)
{
return _canExecute == null || _canExecute(parameter);
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public void Execute(object parameter)
{
_execute(parameter);
}
}
Here is the ButtonBehaviour code:
public static class ButtonBehaviour
{
public static readonly DependencyProperty SearchCommand;
public static readonly DependencyProperty ClearCommand;
public static readonly DependencyProperty EmailCommand;
public static readonly DependencyProperty TextBoxKeyCommand;
static ButtonBehaviour()
{
SearchCommand = EventBehaviourFactory.CreateCommandExecutionEventBehaviour(Button.ClickEvent, "SearchCommand", typeof(ButtonBehaviour));
ClearCommand = EventBehaviourFactory.CreateCommandExecutionEventBehaviour(Button.ClickEvent, "ClearCommand", typeof(ButtonBehaviour));
EmailCommand = EventBehaviourFactory.CreateCommandExecutionEventBehaviour(Button.ClickEvent, "EmailCommand", typeof(ButtonBehaviour));
TextBoxKeyCommand = EventBehaviourFactory.CreateCommandExecutionEventBehaviour(TextBox.KeyDownEvent, "EnterKeyCommand", typeof(ButtonBehaviour));
}
public static void SetSearchCommand(DependencyObject depobj, ICommand value)
{
depobj.SetValue(SearchCommand, value);
}
public static ICommand GetSearchCommand(DependencyObject depobj)
{
return depobj.GetValue(SearchCommand) as ICommand;
}
public static void SetClearCommand(DependencyObject depobj, ICommand value)
{
depobj.SetValue(ClearCommand, value);
}
public static ICommand GetClearCommand(DependencyObject depobj)
{
return depobj.GetValue(ClearCommand) as ICommand;
}
public static void SetEmailCommand(DependencyObject depobj, ICommand value)
{
depobj.SetValue(EmailCommand, value);
}
public static ICommand GetEmailCommand(DependencyObject depobj)
{
return depobj.GetValue(EmailCommand) as ICommand;
}
public static void SetTextBoxKeyCommand(DependencyObject depobj, ICommand value)
{
depobj.SetValue(TextBoxKeyCommand, value);
}
public static ICommand GetTextBoxKeyCommand(DependencyObject depobj)
{
return depobj.GetValue(TextBoxKeyCommand) as ICommand;
}
}
XAML code:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Image Grid.Row="0" Height="84" HorizontalAlignment="Left" Margin="0,5,5,5" Name="imgNexusLogo" Stretch="Fill" VerticalAlignment="Top" Width="600" Source="nexus1bannerlong.png" />
<Grid Grid.Row="1" HorizontalAlignment="Center" Margin="0,5,5,5" VerticalAlignment="Center">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0" Content="Search for Application">
<Label.Foreground>
<SolidColorBrush Color="LightCyan" />
</Label.Foreground>
</Label>
<TextBox Grid.Row="0" Grid.Column="1" Margin="3" Width="500" Text="{Binding AppToSearch}" vm:ButtonBehaviour.TextBoxKeyCommand="{Binding TextBoxKeyPressed}" />
<Button Grid.Row="0" Grid.Column="2" HorizontalAlignment="Right" Width="100" Height="20" Margin="3" Background="LightCyan" Content="Search" ToolTip="Click to filter application list by search value" vm:ButtonBehaviour.SearchCommand="{Binding SearchButtonPressed}" />
<Button Grid.Row="0" Grid.Column="3" HorizontalAlignment="Right" Width="100" Height="20" Margin="3" Background="LightCyan" Content="Clear Search" ToolTip="Click to restore application list to full listing" vm:ButtonBehaviour.ClearCommand="{Binding ClearButtonPressed}"/>
</Grid>
<ListView Grid.Row="2" BorderBrush="Black" HorizontalAlignment="Stretch" ItemsSource="{Binding Path=AppCollection}" SelectedItem="{Binding SelectedApp}">
<ListView.View>
<GridView>
<GridViewColumn Header="Application Name" Width="200" DisplayMemberBinding="{Binding Name}"/>
<GridViewColumn Header="Application Description" Width="631" DisplayMemberBinding="{Binding Description}"/>
<GridViewColumn Header="Application Owner" Width="150" DisplayMemberBinding="{Binding Owner}"/>
</GridView>
</ListView.View>
</ListView>
<Button Grid.Row="3" HorizontalAlignment="Center" Width="200" Height="30" Margin="3" Background="LightCyan" Content="Request Application" ToolTip="Click here to contact app owners" vm:ButtonBehaviour.EmailCommand="{Binding EmailButtonPressed}" />
</Grid>
The Event Behaviour Factory code can be found at http://blog.functionalfun.net/2008/09/hooking-up-commands-to-events-in-wpf.html, I have not made any changed to it.
Anyone know what I'm doing wrong? It's probably simple, but since I'm new to WPF and MVVM, I need to get my head around it.
Thanks!
Follow up
I just did a code behind and handled the event like that. It's not worth all of the headaches to avoid any code behinds, and this project has deadlines that have a higher priority that MVVM ideological purity. For any readers who are interested, here is what I did:
C# code:
private void TextBox_KeyDown(object sender, System.Windows.Input.KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
ApplicationViewModel viewModel = DataContext as ApplicationViewModel;
if (viewModel != null)
{
viewModel.AppToSearch = textboxSearch.Text;
viewModel.SearchButtonPressedExecute(textboxSearch.Text);
}
}
}
XAML:
<TextBox Name="textboxSearch" Grid.Row="0" Grid.Column="1" Margin="3" Width="500" Text="{Binding AppToSearch}" KeyDown="TextBox_KeyDown"/>
I typically use Triggers to bind text box to a command in viewmodel.
<ComboBox Margin="5" ItemsSource="{Binding Configs}" SelectedItem="{Binding SelectedConfig, Mode=TwoWay}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<i:InvokeCommandAction Command="{Binding RetrieveSourceLayersCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</ComboBox>
You need to add reference to this namespace in your xaml: xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" and need to add appropriate assembly in you project reference. In my case I added this assembly: Choose Project > Add Reference. In the Reference Manager dialog box, on the Extensions tab, check the listing for System.Windows.Interactivity. You'll find it under Assemblies > Extensions.

Categories

Resources