I am creating an application where I want to use validation rules, but on some screens there is not enough space to display the resulting error alongside the field which is in error, so I want to put it in a status bar at the bottom of the field.
This sample is from several bits I have pieced together from the web which gives a form which validates using different rules and displays the errors in different ways, but I cant see how to get the error message into the StatusBarItem using XAML. I feel sure there is a simple way to do it. Can anyone help me please?
The sample was written in VS2010 using Framework 4.0.
MainWindow.xaml
<Window x:Class="SampleValidation.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:c="clr-namespace:SWL.Libraries.SysText"
Title="Sample ValidationRule WPF" Height="350" Width="525"
Loaded="Window_Loaded" WindowStartupLocation="CenterScreen">
<Window.Resources>
<ControlTemplate x:Key="validationTemplate">
<!--
<TextBlock Foreground="Red" FontSize="20">***</TextBlock>
-->
<DockPanel LastChildFill="True">
<TextBlock DockPanel.Dock="Right" Foreground="Red" Margin="5" FontSize="8pt" Text="***" />
<AdornedElementPlaceholder />
</DockPanel>
</ControlTemplate>
<Style x:Key="textBoxInError1" TargetType="{x:Type TextBox}">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip"
Value="{Binding RelativeSource={x:Static RelativeSource.Self},
Path=(Validation.Errors)[0].ErrorContent}" />
</Trigger>
</Style.Triggers>
</Style>
<Style x:Key="textBoxInError2" TargetType="{x:Type TextBox}">
<Setter Property="Validation.ErrorTemplate">
<Setter.Value>
<ControlTemplate>
<DockPanel LastChildFill="True">
<TextBlock DockPanel.Dock="Right" Foreground="Red" Margin="5" FontSize="8pt"
Text="{Binding ElementName=MyAdorner, Path=AdornedElement.(Validation.Errors)[0].ErrorContent}" />
<Border BorderBrush="Red" BorderThickness="2">
<AdornedElementPlaceholder Name="MyAdorner" />
</Border>
</DockPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip"
Value="{Binding RelativeSource={RelativeSource Self},
Path=(Validation.Errors)[0].ErrorContent}" />
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid>
<TextBlock Height="24" HorizontalAlignment="Left" Margin="36,73,0,0" Name="textBlock1"
Text="Node Address:" VerticalAlignment="Top" Width="87" />
<TextBlock Height="24" HorizontalAlignment="Left" Margin="36,112,0,0" Name="textBlock2"
Text="Node Name:" VerticalAlignment="Top" Width="78" />
<TextBox Height="24" HorizontalAlignment="Left" Margin="129,70,0,0" Name="textBox1"
VerticalAlignment="Top" Width="119" TextChanged="textBox1_TextChanged"
TabIndex="0" Style="{StaticResource textBoxInError2}"
Validation.ErrorTemplate="{StaticResource validationTemplate}">
<Binding Path="NodeAddress" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<c:NumberRangeRule Min="1" Max="100" />
</Binding.ValidationRules>
</Binding>
</TextBox>
<TextBox Height="24" HorizontalAlignment="Left" Margin="129,109,0,0" Name="textBox2"
VerticalAlignment="Top" Width="119" TextChanged="textBox2_TextChanged"
TabIndex="1" Style="{StaticResource textBoxInError2}">
<Binding Path="NodeName" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<c:NameFormatRule MinLength="6" MaxLength="9" />
</Binding.ValidationRules>
</Binding>
</TextBox>
<StatusBar Height="23" HorizontalAlignment="Stretch" Margin="0,0,0,0"
Name="myStatusBar" VerticalAlignment="Bottom">
<StatusBarItem x:Name="errorStatusBarItem" Content="No errors" />
</StatusBar>
<Button Content="Close" Height="29" HorizontalAlignment="Left" Margin="108,227,0,0"
Name="btnCLOSE" VerticalAlignment="Top" Width="85" Click="btnCLOSE_Click" TabIndex="3" />
<Button Content="Apply" Height="29" HorizontalAlignment="Left" Margin="297,227,0,0"
Name="btnAPPLY" VerticalAlignment="Top" Width="85" Click="btnAPPLY_Click" TabIndex="2" />
</Grid>
</Window>
MainWindow.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
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 SampleValidation {
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window {
public int NodeAddress { get; set; }
public string NodeName { get; set; }
public bool IsAllLoaded { get; set; }
public MainWindow() {
NodeAddress = 1;
NodeName = "freddy";
IsAllLoaded = false;
InitializeComponent();
btnAPPLY.Visibility = System.Windows.Visibility.Hidden;
DataContext = this;
}
private void btnAPPLY_Click(object sender, RoutedEventArgs e) {
// if there are no format errors reported by the validation rules
Validator.ErrorText = "";
if (Validator.IsValid(this))
// Save the data
btnAPPLY.Visibility = System.Windows.Visibility.Hidden; // hide the button indicating nothing new to save
else
MessageBox.Show("Cant Save Changes - Error in form\r\n" + Validator.ErrorText, "Save not allowed", MessageBoxButton.OK, MessageBoxImage.Error);
}
private void btnCLOSE_Click(object sender, RoutedEventArgs e) {
if (btnAPPLY.Visibility != System.Windows.Visibility.Hidden) {
MessageBoxResult myAnswer = MessageBox.Show("Save Changes?", "Confirmation", MessageBoxButton.YesNoCancel);
if (myAnswer == MessageBoxResult.Cancel)
return;
if (myAnswer == MessageBoxResult.Yes)
btnAPPLY_Click(sender, e);
}
this.Close();
}
private void Window_Loaded(object sender, RoutedEventArgs e) {
IsAllLoaded = true;
}
private void ShowModified() {
if (IsAllLoaded)
btnAPPLY.Visibility = System.Windows.Visibility.Visible;
} // ShowModified
private void textBox2_TextChanged(object sender, TextChangedEventArgs e) {
ShowModified();
}
private void textBox1_TextChanged(object sender, TextChangedEventArgs e) {
ShowModified();
}
} // class MainWindow
public static class Validator {
public static string ErrorText { get; set; }
static Validator() {
ErrorText = "";
}
public static bool IsValid(DependencyObject parent) {
// Validate all the bindings on the parent
bool valid = true;
LocalValueEnumerator localValues = parent.GetLocalValueEnumerator();
while (localValues.MoveNext()) {
LocalValueEntry entry = localValues.Current;
if (BindingOperations.IsDataBound(parent, entry.Property)) {
Binding binding = BindingOperations.GetBinding(parent, entry.Property);
if (binding.ValidationRules.Count > 0) {
BindingExpression expression = BindingOperations.GetBindingExpression(parent, entry.Property);
expression.UpdateSource();
if (expression.HasError) {
ErrorText = expression.ValidationError.ErrorContent.ToString();
valid = false;
}
}
}
}
// Validate all the bindings on the children
System.Collections.IEnumerable children = LogicalTreeHelper.GetChildren(parent);
foreach (object obj in children) {
if (obj is DependencyObject) {
DependencyObject child = (DependencyObject)obj;
if (!IsValid(child)) {
valid = false;
}
}
}
return valid;
}
} // class Validator
ValidationRules.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Globalization;
using System.Windows.Controls;
namespace SWL.Libraries.SysText {
public class NumberRangeRule : ValidationRule {
private int _min;
private int _max;
public NumberRangeRule() { }
public int Min {
get { return _min; }
set { _min = value; }
}
public int Max {
get { return _max; }
set { _max = value; }
}
public override ValidationResult Validate(object value, CultureInfo cultureInfo) {
int val = 0;
try {
if (((string)value).Length > 0)
val = Int32.Parse((String)value);
} catch (Exception e) {
return new ValidationResult(false, "Illegal Characters or " + e.Message);
}
if ((val < Min) || (val > Max)) {
return new ValidationResult(false, "Please Enter Number in Range: " + Min + " - " + Max + ".");
} else {
return new ValidationResult(true, null);
}
}
}
public class NameFormatRule : ValidationRule {
private int _minLength;
private int _maxLength;
public NameFormatRule() { }
public int MinLength {
get { return _minLength; }
set { _minLength = value; }
}
public int MaxLength
get { return _maxLength; }
set { _maxLength = value; }
}
public override ValidationResult Validate(object value, CultureInfo cultureInfo) {
try {
if (((string)value).Length > 0) {
if (((string)value).Length < MinLength || ((string)value).Length > MaxLength)
return new ValidationResult(false, String.Format ("Enter a string of {0} to {1} characters in length", MinLength, MaxLength));
return new ValidationResult(true, null);
}
return new ValidationResult(true, null);
} catch (Exception e) {
return new ValidationResult(false, "Illegal Characters or " + e.Message);
}
}
}
}
UPDATE
I recently had to handle multiple error messages that were displayed when the mouse hovered an error icon. I made my viewmodel implement IDataErrorInfo. I then created a class-level Dictionary, keyed by a unique identifier for each control (I just used an enum), in my viewmodel to hold one error per control. Then I created a public property of type string and called it ErrorText. The getter of ErrorText iterates the errors dictionary and builds out all the errors into one string.
Here's an example, it may need tweaked. I know this is extremely simplified but should get you going in a direction. For complex validation, you can still validation using ValidationRule objects you create and then just check the IsValid property of the ValidationResult object returned.
// NOTE: The enum member name matches the name of the property bound to each textbox
public enum ControlIDs
{
TextBox1Value = 0,
TextBox2Value
}
public class MyViewModel : IDataErrorInfo
{
private readonly Dictionary<ControlIDs, string> errors;
public string ErrorText
{
get
{
if (errors.ContainsKey(ControlIDs.TextBox1) && errors.ContainsKey(ControlIDs.TextBox2))
{
return "Errors: " + errors[ControlsIDs.TextBox1] + ", " + errors[ControlsIDs.TextBox2];
}
if (errors.ContainsKey(ControlIDs.TextBox1))
{
return "Error: " + errors[ControlsIDs.TextBox1];
}
if (errors.ContainsKey(ControlIDs.TextBox2))
{
return "Error: " + errors[ControlsIDs.TextBox2];
}
}
}
public MyViewModel()
{
errors = new Dictionary<ControlIDs, string>();
}
private void UpdateErrorCollection(ControlIDs fieldKey, string error)
{
if (errors.ContainsKey(fieldKey))
{
errors[fieldKey] = error;
}
else
{
errors.Add(fieldKey, error);
}
OnPropertyChanged("ErrorText");
}
#region IDataErrorInfo
public string Error
{
get { throw new NotImplementedException(); }
}
public string this[string columnName]
{
string error = string.Empty;
if (columnName == ControlIDs.TextBox1Value.ToString())
{
if (string.IsNullOrWhiteSpace(TextBox1Value))
{
error = "TextBox1 must contain a value";
UpdateErrorCollection(ControlIDs.TextBox1Value, error);
}
else
{
errors.Remove(ControlIDs.TextBox1Value);
}
}
else if (columnName == ControlIDs.TextBox2Value))
{
if (string.IsNullOrWhiteSpace(TextBox2Value))
{
error = "TextBox2 must contain a value";
UpdateErrorCollection(ControlIDs.TextBox2Value, error);
}
else
{
errors.Remove(ControlIDs.TextBox2Value);
}
}
// returning null indicates success
return !string.IsNullOrWhiteSpace(error) ? error : null;
}
#endregion
}
Now just bind your StatusBarItem TextBlock to the ErrorText property.
Related
I want to validate a textbox if something is wrong. The idea is if something is wrong than the next TextBox should have a warning image.
<TextBox Text="{Binding Age, UpdateSourceTrigger=PropertyChanged}">
<Validation.ErrorTemplate>
<ControlTemplate>
<StackPanel>
<!-- Placeholder for the TextBox itself -->
<AdornedElementPlaceholder x:Name="textBox"/>
<image source="some-Image.png" width="20" Height="20" />
</StackPanel>
</ControlTemplate>
</Validation.ErrorTemplate>
</TextBox>
But the thing is the image is not showing, it only shows the border of the icon.
Am I using AdornedElementPlaceholder correctly?
Tested solution that works and displays the image when error occurs:
<TextBox BorderThickness="0.8">
<Validation.ErrorTemplate>
<ControlTemplate>
<StackPanel>
<AdornedElementPlaceholder/>
<Image Source="Image.jpg" Width="20" Height="20"/>
</StackPanel>
</ControlTemplate>
</Validation.ErrorTemplate>
<TextBox.Text>
<Binding Path="ValidationTest" UpdateSourceTrigger="PropertyChanged" Mode="TwoWay" ValidatesOnDataErrors="True">
<Binding.ValidationRules>
<validation:IntegerValidationRule ValidationStep="CommittedValue" Min="1" Max="99999999"/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
And here is the validation rule that I have used for this example:
public class IntegerValidationRule : ValidationRule
{
private int _min = int.MinValue;
private int _max = int.MaxValue;
private string _fieldName = "Field";
private string _customMessage = String.Empty;
public int Min
{
get { return _min; }
set { _min = value; }
}
public int Max
{
get { return _max; }
set { _max = value; }
}
public string FieldName
{
get { return _fieldName; }
set { _fieldName = value; }
}
public string CustomMessage
{
get { return _customMessage; }
set { _customMessage = value; }
}
public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
{
int num = 0;
var val = (value as BindingExpression).DataItem;
if (!int.TryParse(value.ToString(), out num))
return new ValidationResult(false, $"{FieldName} must contain an integer value.");
if (num < Min || num > Max)
{
if (!String.IsNullOrEmpty(CustomMessage))
return new ValidationResult(false, CustomMessage);
return new ValidationResult(false, $"{FieldName} must be between {Min} and {Max}.");
}
return new ValidationResult(true, null);
}
}
Which is just modified example from MSDN Docs
Some notes:
You didn't provide the validation rule so I am assuming it works as expected and produces valid validation result.
The Binding of your TextBox.Text property doesn't include the Validation rule.
I have found many pages that bear on this in one way or another, but have still not discovered how to achieve it. Here is my XAML:
<TreeView ItemsSource="{Binding Document}" HorizontalAlignment="Stretch" BorderThickness="0" Background="#FFC2A2A2">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
<Setter Property="FontWeight" Value="Normal" />
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="FontWeight" Value="Bold" />
</Trigger>
</Style.Triggers>
</Style>
</TreeView.ItemContainerStyle>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal">
<Image Source="{Binding ImageFile}" Height="16" Width="16"/>
<TextBlock Text="{Binding Name}" Margin="5,0,0,0"/>
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
<TreeView.ContextMenu>
<ContextMenu>
<MenuItem Header="Add" Command="{Binding AddCommand}"/>
<MenuItem Header="Delete" Command="{Binding DeleteCommand}"/>
</ContextMenu>
</TreeView.ContextMenu>
</TreeView>
I implemented the AddCommand and DeleteCommand based roughly on the Search button implementation in this.
Both commands require the SelectedItem from the tree, so I implemented it in the tree MVVM, added a pointer to the tree MVVM to each item MVVM, and maintain it via the IsSelected property in the item MVVM.
public bool IsSelected
{
get { return mIsSelected; }
set
{
if (value != mIsSelected)
{
mIsSelected = value;
this.OnPropertyChanged("IsSelected");
}
if (mIsSelected)
{
mDocViewModel.SelectedItem = this;
}
}
}
(We use mAbc for data members, rather than _abc.)
This all works. However, the context menus have a context. Based on which is selected, the AddCommand may not be valid, and I want that represented as disabled and enabled in the view.
I put my tests for this condition in the CanExecute method of each command. But at run time, CanExecute seems never to be invoked, and both menu item always appear disabled.
Is there a way to get this done? Is there a simple way?
Thanks,
Art
LATER:
Editing my question appears to be the way to make a longer reply. Here, then, is one of the Command classes ... with respect to the CanExecute mentioned afterwards.
#region DeleteCommand
public ICommand DeleteCommand
{
get { return mDeleteCommand; }
}
void DeleteNode()
{
if (mSelectedItem != null)
{
mSelectedItem.Remove();
mSelectedItem = null;
}
}
private class DeleteNodeCommand : RoutedCommand
{
DocumentRulesViewModel mDocumentViewModel;
public DeleteNodeCommand (DocumentRulesViewModel _docViewModel)
{
mDocumentViewModel = _docViewModel;
}
void SelectedItem_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
throw new NotImplementedException();
}
public bool CanExecute(object parameter)
{
DesignObjectViewModel current = mDocumentViewModel.SelectedItem;
return (current != null);
}
event EventHandler CanExecuteChanged
{
// I intentionally left these empty because
// this command never raises the event, and
// not using the WeakEvent pattern here can
// cause memory leaks. WeakEvent pattern is
// not simple to implement, so why bother.
add { }
remove { }
}
public void Execute(object parameter)
{
mDocumentViewModel.DeleteNode();
}
public event PropertyChangedEventHandler PropertyChanged;
}
#endregion
I didn't do anything with the event stuff at the bottom, just copied it from an example. And, in that example, the command would always be valid. So maybe the issue lies there.
But I did some prowling for CanExecuteChange, and did not really see what to do with it.
Jim, I guess all I can do it show it all (I'll have to omit the application/model parts, of course.
Main xaml:
<Window x:Class="xDesign.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:xDesign.View"
Title="{StaticResource thisAppName}" Height="350" Width="525">
<DockPanel>
<Menu VerticalAlignment="Top" DockPanel.Dock="Top" BorderThickness="0">
<MenuItem Header="{StaticResource fileMenu}" Name="FileMenu">
<MenuItem Header="{StaticResource newFileMenu}" Click="NewDocumentMenuItem_Click" Name="FileMenuNewDoc"/>
<MenuItem Header="{StaticResource openFileMenu}" Click="OpenDocumentMenuItem_Click" Name="FileMenuOpenDoc" />
<MenuItem Header="{StaticResource closeFileMenu}" Click="CloseDocumentMenuItem_Click" IsEnabled="False" Name="FileMenuCloseDoc" />
<Separator />
<MenuItem Name="FileMenuCheckout" Header="{StaticResource checkoutFileMenu}" Click="FileMenuCheckout_Click"/>
<MenuItem Name="FileMenuCheckin" Header="{StaticResource checkinFileMenu}" Click="FileMenuCheckin_Click" IsEnabled="False"/>
<MenuItem Name="FileMenuDeleteFromServer" Header="{StaticResource deleteFromServerFileMenu}" Click="FileMenuDeleteFromServer_Click" IsEnabled="False"/>
<MenuItem Name="FileMenuLogon" Header="{StaticResource logonFileMenu}" Click="FileMenuLogon_Click"/>
<MenuItem Name="FileMenuLogoff" IsEnabled="False" Header="{StaticResource logoffFileMenu}" Click="FileMenuLogoff_Click"/>
</MenuItem>
<MenuItem Header="{StaticResource editMenu}" IsEnabled="False" Name="EditMenu">
<MenuItem Header="{StaticResource findEditMenu}" Click="FindEditMenuItem_Click"/>
</MenuItem>
<MenuItem Header="{StaticResource viewMenu}" IsEnabled="False" Name="ViewMenu">
<MenuItem Header="{StaticResource expandViewMenu}" Click="ExpandViewMenuItem_Click"/>
<MenuItem Header="{StaticResource collapseViewMenu}" Click="CollapseViewMenuItem_Click"/>
</MenuItem>
</Menu>
<Grid Name="DesignPanel" DockPanel.Dock="Top">
<Grid.ColumnDefinitions >
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions >
<local:DocumentTreeView x:Name="DocTreeView" Grid.Column="0"/>
<GridSplitter Grid.Column="0" HorizontalAlignment="Right" VerticalContentAlignment="Stretch" Width="3" ResizeDirection="Columns" />
<WebBrowser x:Name="objectPreviewBrowser" Grid.Column="1" Margin="6,6,0,0" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" OpacityMask="#FF9B8E8E"/>
</Grid>
</DockPanel>
</Window>
Control xaml:
<UserControl x:Class="xDesign.View.DocumentTreeView"
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"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<TreeView ItemsSource="{Binding Document}" HorizontalAlignment="Stretch" BorderThickness="0" Background="#FFC2A2A2">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
<Setter Property="FontWeight" Value="Normal" />
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="FontWeight" Value="Bold" />
</Trigger>
</Style.Triggers>
</Style>
</TreeView.ItemContainerStyle>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal">
<Image Source="{Binding ImageFile}" Height="16" Width="16"/>
<TextBlock Text="{Binding Name}" Margin="5,0,0,0"/>
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
<TreeView.ContextMenu>
<ContextMenu>
<MenuItem Header="Add rule" Command="{Binding AddRuleCommand}"/>
<MenuItem Header="Delete" Command="{Binding DeleteCommand}"/>
</ContextMenu>
</TreeView.ContextMenu>
</TreeView>
</UserControl>
Primary view model:
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows.Input;
using xDesign.Actions;
using xDesign.API.Model;
namespace xDesign.ViewModel
{
public class DocumentRulesViewModel : INotifyPropertyChanged
{
#region data members
DesignObjectViewModel mRootObject = null;
ObservableCollection<DesignObjectViewModel> mDocument = null;
DesignObjectViewModel mSelectedItem = null;
ICommand mDeleteCommand = null;
ICommand mAddRuleCommand = null;
#endregion
#region consructors
public DocumentRulesViewModel(DocumentObject _rootObject)
{
mRootObject = new DesignObjectViewModel(_rootObject, this);
mDocument = new ObservableCollection<DesignObjectViewModel>
(new DesignObjectViewModel[] { mRootObject });
mRootObject.IsExpanded = true; // We start with the top node expanded
mDeleteCommand = new DeleteNodeCommand(this);
mAddRuleCommand = new AddRuleCommandClass(this);
}
~DocumentRulesViewModel()
{
Close();
}
public void Close()
{
Document = null;
}
#endregion
#region properties
public ObservableCollection<DesignObjectViewModel> Document
{
get { return mDocument; }
set
{
if (value != mDocument)
{
mDocument = value;
this.OnPropertyChanged("Document");
}
}
}
public DesignObjectViewModel SelectedItem
{
get { return mSelectedItem; }
set
{
if (value != mSelectedItem)
{
mSelectedItem = value;
this.OnPropertyChanged("SelectedItem");
}
}
}
public IDesignObject CurrentDesignObject
{
get
{
if (mSelectedItem == null)
{
return null;
}
else
{
return mSelectedItem.DesignObject;
}
}
set
{
DesignObjectViewModel dovm = SearchForNode(value);
if (dovm != null)
{
if (dovm.Parent != null && !dovm.Parent.IsExpanded)
{
dovm.Parent.IsExpanded = true;
}
dovm.IsSelected = true;
}
}
}
#endregion
#region DeleteCommand
public ICommand DeleteCommand
{
get { return mDeleteCommand; }
}
public void DeleteItem ()
{
DesignObjectViewModel node = this.SelectedItem;
node.Remove();
}
private class DeleteNodeCommand : RoutedCommand
{
DocumentRulesViewModel mTree;
public DeleteNodeCommand(DocumentRulesViewModel _tree)
{
mTree = _tree;
}
public bool CanExecute(object parameter)
{
DesignObjectViewModel node = mTree.SelectedItem;
return (node != null);
}
public void Execute(object parameter)
{
mTree.DeleteItem();
}
// allows for constant updating if the event can execute or not.
public event EventHandler CanExecuteChanged
{
add
{
CommandManager.RequerySuggested += value;
}
remove
{
CommandManager.RequerySuggested -= value;
}
}
public void RaiseCanExecuteChanged()
{
// we should not have to reevaluate every can execute.
// but since there are too many places in product code to verify
// we will settle for all or nothing.
CommandManager.InvalidateRequerySuggested();
}
}
#endregion
#region AddRuleCommand
public ICommand AddRuleCommand
{
get { return mAddRuleCommand; }
}
void AddRule()
{
int index = -1; // Where to insert; -1 = inside selected item
if (mSelectedItem.Parent != null)
{
index = mSelectedItem.Parent.Children.IndexOf(mSelectedItem) + 1; // Insert after selected item
}
// Call the application logic
IDesignObject dobj = DocStructureManagement.AddRule(mSelectedItem.DesignObject, ref index);
if (dobj != null)
{
DesignObjectViewModel newItemParent;
if (index == -1)
{
newItemParent = mSelectedItem;
index = 0;
}
else
{
newItemParent = mSelectedItem.Parent;
}
DesignObjectViewModel newItem = new DesignObjectViewModel(dobj, this, newItemParent);
newItemParent.InsertChild(newItem, index);
}
}
private class AddRuleCommandClass : RoutedCommand
{
DocumentRulesViewModel mTree;
public AddRuleCommandClass(DocumentRulesViewModel _tree)
{
mTree = _tree;
}
public bool CanExecute(object parameter)
{
DesignObjectViewModel node = mTree.SelectedItem;
return (node != null && node.DesignObject.CanContainOrPrecede(eDesignNodeType.ContentRule));
}
public void Execute(object parameter)
{
mTree.AddRule();
}
// allows for constant updating if the event can execute or not.
public event EventHandler CanExecuteChanged
{
add
{
CommandManager.RequerySuggested += value;
}
remove
{
CommandManager.RequerySuggested -= value;
}
}
public void RaiseCanExecuteChanged()
{
// we should not have to reevaluate every can execute.
// but since there are too many places in product code to verify
// we will settle for all or nothing.
CommandManager.InvalidateRequerySuggested();
}
}
#endregion
#region Search
private DesignObjectViewModel SearchForNode(IDesignObject _dobj)
{
return SearchNodeForNode(mRootObject, _dobj);
}
private DesignObjectViewModel SearchNodeForNode(DesignObjectViewModel _node, IDesignObject _dobj)
{
if (_node.DesignObject == _dobj)
{
return _node;
}
foreach (DesignObjectViewModel child in _node.Children)
{
DesignObjectViewModel childNode = SearchNodeForNode(child, _dobj);
if (childNode != null)
{
return childNode;
}
}
return null;
}
#endregion
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
#endregion // INotifyPropertyChanged Members
}
}
TreeViewItem view model:
using System;
using System.Collections.ObjectModel;
using System.Linq;
using xDesign.API.Model;
using xDesign.Actions;
using System.ComponentModel;
using System.Windows.Input;
namespace xDesign.ViewModel
{
public class DesignObjectViewModel : INotifyPropertyChanged
{
#region data
DocumentRulesViewModel mDocViewModel = null;
IDesignObject mDesignObject = null;
DesignObjectViewModel mParent = null;
ObservableCollection<DesignObjectViewModel> mChildren = null;
bool mIsSelected = false;
bool mIsExpanded = false;
#endregion
#region constructors
public DesignObjectViewModel(IDesignObject _dobj, DocumentRulesViewModel _docViewModel)
: this(_dobj, _docViewModel, null)
{
}
public DesignObjectViewModel(IDesignObject _dobj, DocumentRulesViewModel _docViewModel, DesignObjectViewModel _parent)
{
mDesignObject = _dobj;
mDocViewModel = _docViewModel;
mParent = _parent;
if (_dobj.Type != eDesignNodeType.ContentGroup)
{
mChildren = new ObservableCollection<DesignObjectViewModel>(
(from child in mDesignObject.Children
select new DesignObjectViewModel(child, mDocViewModel, this))
.ToList<DesignObjectViewModel>());
}
else
{
ContentHolder ch = (ContentHolder)_dobj;
mChildren = new ObservableCollection<DesignObjectViewModel>(
(from child in ch.Contents
select new DesignObjectViewModel(child, mDocViewModel, this))
.ToList<DesignObjectViewModel>());
}
}
#endregion
#region properties
public ObservableCollection<DesignObjectViewModel> Children
{
get { return mChildren; }
}
public DesignObjectViewModel Parent
{
get { return mParent; }
}
public String Name
{
get { return mDesignObject.Name; }
}
public IDesignObject DesignObject
{
get { return mDesignObject; }
}
public Type DataType
{
get { return mDesignObject.GetType(); }
}
// Can we use DataType for this, and task the View with finding a corresponding image?
// And do we want to? We could end up with file names that include Model type names.
// Better? Worse? The same?
public String ImageFile
{
get { return GetImageUri(mDesignObject); }
}
public bool IsExpanded
{
get { return mIsExpanded; }
set
{
if (value != mIsExpanded)
{
mIsExpanded = value;
this.OnPropertyChanged("IsExpanded");
}
// Expand all the way up to the root.
if (mIsExpanded && mParent != null)
mParent.IsExpanded = true;
}
}
public bool IsSelected
{
get { return mIsSelected; }
set
{
if (value != mIsSelected)
{
mIsSelected = value;
this.OnPropertyChanged("IsSelected");
if (mIsSelected)
{
mDocViewModel.SelectedItem = this;
}
CommandManager.InvalidateRequerySuggested();
}
}
}
#endregion
#region public methods
public void Remove()
{
DocStructureManagement.DeleteNode(mDesignObject); // Remove from application
if (mParent != null) // Remove from ViewModel
{
mParent.Children.Remove(this);
mParent.OnPropertyChanged("Children");
}
}
public void InsertChild(DesignObjectViewModel _newChild, int _insertIndex)
{
Children.Insert(_insertIndex, _newChild);
this.OnPropertyChanged("Children");
}
#endregion
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
#endregion // INotifyPropertyChanged Members
internal static string GetImageUri(IDesignObject _dobj)
{
string name = null;
switch (_dobj.Type)
{
case eDesignNodeType.Document:
name = "xDesign.ico";
break;
case eDesignNodeType.ContentRule:
name = "Content Rule.png";
break;
case eDesignNodeType.Section:
name = "section rule.png";
break;
case eDesignNodeType.Table:
name = "Table Rule.bmp";
break;
case eDesignNodeType.Read:
name = "Read Rule.bmp";
break;
case eDesignNodeType.Goto:
name = "Goto Rule.bmp";
break;
case eDesignNodeType.Label:
name = "Label Rule.bmp";
break;
case eDesignNodeType.ContentGroup:
name = "ContentGroup.png";
break;
case eDesignNodeType.Content:
name = "content.png";
break;
case eDesignNodeType.Criteria:
name = "Criteria.bmp";
break;
}
if (name == null)
{
throw new Exception("No image found for " + _dobj.Name);
}
return string.Format(#"C:\DEVPROJECTS\XDMVVM\XDMVVM\Images\{0}", name);
}
}
}
Finally, a code snippet from main window code behind, where I create and connect the main view model.
mDocumentRulesViewModel = new DocumentRulesViewModel(mCurrentDocument);
this.DocTreeView.DataContext = mDocumentRulesViewModel;
Again, I set breakpoints in the CanExecute method of each of the two command classes, and control never stops there.
I created a tiny sample project, similar to yours to solve this. I was able to have the context menu CanExecute behave correctly. If you emulate this style you will be able to solve your problem.
MainWindow.Xaml:
<Window x:Class="CommandChangesInTreeViewContextMenu.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"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Button Command="{Binding AddCommand}">Add Command </Button>
<TreeView Grid.Row="1"
ItemsSource="{Binding MasterList}" HorizontalAlignment="Stretch" BorderThickness="0" Background="#FFC2A2A2">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
</TreeView.ItemContainerStyle>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate >
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}" Margin="5,0,0,0"/>
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
<TreeView.ContextMenu>
<ContextMenu>
<MenuItem Header="Add" Command="{Binding AddCommand}"/>
<!--<MenuItem Header="Delete" Command="{Binding DeleteCommand}"/>-->
</ContextMenu>
</TreeView.ContextMenu>
</TreeView>
<Button Grid.Row="2"
Command="{Binding ClearSelectionsCommand}">Clear Selections </Button>
</Grid>
</Window>
The DataContext ViewModel for the MainWindow.Xaml is TreeViewModel:
public class TreeViewModel : ObservableObject
{
private ObservableCollection<MasterItem> _masterList;
private ICommand _addCommand;
private ICommand _clearSelectionsCommand;
public ObservableCollection<MasterItem> MasterList
{
get { return _masterList; }
set
{
if (_masterList != value)
{
_masterList = value;
OnPropertyChanged("MasterList");
}
}
}
public ICommand AddCommand
{
get
{
if (_addCommand == null)
{
_addCommand = new RelayCommand<object>(Add, CanExecuteAddCommand);
}
return _addCommand;
}
}
public ICommand ClearSelectionsCommand
{
get
{
if (_clearSelectionsCommand == null)
{
_clearSelectionsCommand = new RelayCommand<object>(ClearSelections);
}
return _clearSelectionsCommand;
}
}
public TreeViewModel()
{
MasterList = new ObservableCollection<MasterItem>
{
new MasterItem("sup"), new MasterItem("hi"), new MasterItem("test"), new MasterItem("yo")
};
}
private void Add(object o)
{
// does nothing
}
private void ClearSelections(object o)
{
foreach (var mItem in MasterList)
{
mItem.IsSelected = false;
}
}
private bool CanExecuteAddCommand(object o)
{
return MasterList.Any(mItem => mItem.IsSelected == true);
}
}
The MasterItem class which are the objects in your MasterList:
MasterItem.cs:
public class MasterItem : ObservableObject
{
private string _name;
private bool _isSelected;
public string Name
{
get { return _name; }
set
{
if (_name != value)
{
_name = value;
OnPropertyChanged("Name");
}
}
}
public bool IsSelected
{
get { return _isSelected; }
set
{
if (_isSelected != value)
{
_isSelected = value;
OnPropertyChanged("IsSelected");
CommandManager.InvalidateRequerySuggested();
}
}
}
public MasterItem(string name)
{
Name = name;
IsSelected = false;
}
}
**Note that When IsSelected is set it will InvalidateRequerySuggested() and work properly. =) **
Supporting Classes, RelayCommand, and ObservableObject
/// <summary>
/// RelayCommand
///
/// General purpose command implementation wrapper. This is an alternative
/// to multiple command classes, it is a single class that encapsulates different
/// business logic using delegates accepted as constructor arguments.
/// </summary>
/// <typeparam name="T"></typeparam>
public class RelayCommand<T> : ICommand
{
private static bool CanExecute(T paramz)
{
return true;
}
private readonly Action<T> _execute;
private readonly Func<T, bool> _canExecute;
/// <summary>
/// Relay Command
///
/// Stores the Action to be executed in the instance field variable. Also Stores the
/// information about IF it canexecute in the instance field variable. These executing
/// commands can be sent from other methods in other classes. Hence the lambda expressions.
/// Tries to be as generic as possible T type as parameter.
/// </summary>
/// <param name="execute">Holds the method body about what it does when it executes</param>
/// <param name="canExecute">Holds the method body conditions about what needs to happen for the ACTION
/// Execute to execute. If it fails it cannot execute. </param>
public RelayCommand(Action<T> execute, Func<T, bool> canExecute = null)
{
if (execute == null)
throw new ArgumentNullException("execute");
_execute = execute;
_canExecute = canExecute ?? CanExecute;
}
public bool CanExecute(object parameter)
{
return _canExecute(TranslateParameter(parameter));
}
// allows for constant updating if the event can execute or not.
public event EventHandler CanExecuteChanged
{
add
{
if (_canExecute != null)
CommandManager.RequerySuggested += value;
}
remove
{
if (_canExecute != null)
CommandManager.RequerySuggested -= value;
}
}
public void Execute(object parameter)
{
_execute(TranslateParameter(parameter));
}
private T TranslateParameter(object parameter)
{
T value = default(T);
if (parameter != null && typeof(T).IsEnum)
value = (T)Enum.Parse(typeof(T), (string)parameter);
else
value = (T)parameter;
return value;
}
public void RaiseCanExecuteChanged()
{
// we should not have to reevaluate every can execute.
// but since there are too many places in product code to verify
// we will settle for all or nothing.
CommandManager.InvalidateRequerySuggested();
}
}
/// <summary>
/// Class is based on two delegates; one for executing the command and another for returning the validity of the command.
/// The non-generic version is just a special case for the first, in case the command has no parameter.
/// </summary>
public class RelayCommand : RelayCommand<object>
{
public RelayCommand(Action execute, Func<bool> canExecute = null)
: base(obj => execute(),
(canExecute == null ?
null : new Func<object, bool>(obj => canExecute())))
{
}
}
ObservableObject:
public abstract class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
protected bool SetProperty<T>(ref T field, T value, Expression<Func<T>> expression)
{
// Allows a comparison for generics. Otherwise could just say x == y ?
if (!EqualityComparer<T>.Default.Equals(field, value))
{
field = value;
var lambda = (LambdaExpression)expression;
MemberExpression memberExpr;
if (lambda.Body is UnaryExpression)
{
var unaryExpr = (UnaryExpression)lambda.Body;
memberExpr = (MemberExpression)unaryExpr.Operand;
}
else
{
memberExpr = (MemberExpression)lambda.Body;
}
OnPropertyChanged(memberExpr.Member.Name);
return true;
}
return false;
}
}
Note that ObservableObject and RelayCommand are just helpers and not necessary to generating the solution. Mainly look at MainWindow.Xaml, TreeViewModel, and MasterItem. I hope this helps!
Picture of the disabled context menu when IsSelected is set to false for all the MasterItems in MasterList
Example of using a RelayCommand:
in your constructor
public PrimaryViewModel()
{
ICommand bob = new RelayCommand(CommandMethodThatDoesStuff,CanExecuteCommandMethod);
}
private void CommandMethodThatDoesStuff(object o)
{
// do your work
}
private bool CanExecuteCommandMethod(object o)
{
return IsSelected;
}
You are on the right track with the Commands and implementing CanExecute. I have had a similar problem. The commands will not always update their CanExecute immediately. If you want to have all of your commands update, a simple brute force solution is to add a call to CommandManager.InvalidateRequerySuggested().
public bool IsSelected
{
get { return mIsSelected; }
set
{
if (value != mIsSelected)
{
mIsSelected = value;
this.OnPropertyChanged("IsSelected");
}
if (mIsSelected)
{
mDocViewModel.SelectedItem = this;
CommandManager.InvalidateRequerySuggested();
}
}
}
This will call invalidate arrange on all of your commands and force the logic on your commands, CanExecute boolean methods to refresh their state on the UI.
There are two ways to do this, that I can think of.
1 - put the context menu on your HierarchicalDataTemplate. This means the DataContext for the context menu will be the item from the tree. This can be nice because then, for example, the AddCommand is close to where things need to be added. You don't need to track the selected item in this case.
2 - bind the IsEnabled from the MenuItem to an "IsEnabled" property in your VM, and then update it when the selection changes. This is less nice, but can be tidy. If you are only doing single selection, and you already have a property for the selected item in your VM (which you probably should have already) then you can just bind it to something like {Binding SelectedItem.IsEnabled} or something.
ContextMenus aren't part of the visible tree, so binding directly to the view models won't work. The way around it is to use a BindingProxy as explained on this page:
<TreeView ItemsSource="{Binding Items}" >
<TreeView.Resources>
<local:BindingProxy x:Key="Proxy" Data="{Binding}"/>
</TreeView.Resources>
<TreeView.ContextMenu>
<ContextMenu DataContext="{Binding Path=Data, Source={StaticResource Proxy}}">
<MenuItem Header="Add" Command="{Binding AddCommand}"/>
</ContextMenu>
</TreeView.ContextMenu>
</TreeView>
Alternatively if each tree item has it's own view model then you can add the command handlers to the items themselves and bind relative to the placement target:
<TreeView ItemsSource="{Binding Items}" >
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="ContextMenu">
<Setter.Value>
<ContextMenu DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}">
<MenuItem Header="Add" Command="{Binding AddCommand}"/>
</ContextMenu>
</Setter.Value>
</Setter>
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
I am writing simple application,
The UI has two textboxes, for Username & Password and button to submit the information.
I wanted to use routed commands instead of buttonclick event.
Username should contain alphanumeric characters only, if user enter any other special characters, it should display a text saying invalid characters entered.
so I wanted to bind the visibility and content of that textblock based on the validation done on Username textbox field.
can any one help me on how to achieve this?
Below is code I have made but it is not working as expected. can any one help me where I am doing wrong?
Below my mainWindow.xaml
using System.Windows;
using System.Windows.Input;
using System.ComponentModel;
namespace ExcelUtility
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
ViewModel viewModelObj = new ViewModel();
public MainWindow()
{
InitializeComponent();
}
void navigatePageExecuted(object target, ExecutedRoutedEventArgs e)
{
SubmitUserDetails(txtUserName.Text + ";" + txtPassword);
}
void navigatePageCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
if (!string.IsNullOrWhiteSpace(txtUserName.Text))
{
viewModelObj.Username = txtUserName.Text;
}
e.CanExecute = viewModelObj.VaidUserName; }
private void SubmitUserDetails(string credentials)
{
this.Cursor = Cursors.Wait;
prgValidate.Visibility = Visibility.Visible;
MainGrid.IsEnabled = false;
BackgroundWorker worker = new BackgroundWorker();
worker.DoWork += worker_DoWork;
worker.RunWorkerCompleted += worker_RunWorkerCompleted;
worker.RunWorkerAsync(credentials);
}
void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
prgValidate.Visibility = Visibility.Collapsed;
string Result = (string)e.Result;
MessageBox.Show(Result); //Here I need to call some other functions based on return value for simplicity i have changed
}
void worker_DoWork(object sender, DoWorkEventArgs e)
{
string[] credentials = e.Argument.ToString().Split(';');
e.Result = viewModelObj.validateCredentials(credentials[0], credentials[1]);
}
}
}
This is my xaml
<Window x:Class="ExcelUtility.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ExcelUtility"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<local:BoolToVisibleOrHidden x:Key="BoolToVisibilityConverter"/>
</Window.Resources>
<Window.CommandBindings>
<CommandBinding Command="{x:Static local:CommandsLibrary.navigatePageCommand}" Executed="navigatePageExecuted" CanExecute="navigatePageCanExecute"/>
</Window.CommandBindings>
<Grid Name="MainGrid">
<TextBlock Height="23" HorizontalAlignment="Left" Margin="40,44,0,0" Name="tbUserName" Text="Username" VerticalAlignment="Top" />
<TextBox Height="23" HorizontalAlignment="Left" Margin="136,42,0,0" Name="txtUserName" VerticalAlignment="Top" Width="163" Text="{Binding Username, Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Height="23" HorizontalAlignment="Left" Margin="138,19,0,0" Name="tbNotify" Text="{Binding Notification, Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Top" Width="161" Visibility="{Binding NotVaidUserName,Converter={StaticResource BoolToVisibilityConverter}}" />
<TextBox Height="23" HorizontalAlignment="Left" Margin="138,98,0,0" Name="txtPassword" VerticalAlignment="Top" Width="161" />
<TextBlock Height="23" HorizontalAlignment="Left" Margin="44,107,0,0" Name="tbPassword" Text="Password" VerticalAlignment="Top" Width="65" />
<Button Command="{x:Static local:CommandsLibrary.navigatePageCommand}" Content="Submit" Height="23" HorizontalAlignment="Left" Margin="172,167,0,0" Name="btnSubmit" VerticalAlignment="Top" Width="109" />
<ProgressBar Height="24" IsIndeterminate="True" Visibility="Collapsed" HorizontalAlignment="Left" Margin="52,232,0,0" Name="prgValidate" VerticalAlignment="Top" Width="257" />
</Grid>
This is my viewModel
using System;
using System.Text.RegularExpressions;
using System.ComponentModel;
using System.Windows.Data;
using System.Windows;
namespace ExcelUtility
{
public class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private bool _notVaidUserName;
public bool NotVaidUserName
{
get { return _notVaidUserName; }
set
{
_notVaidUserName = value;
RaisePropertyChanged("NotVaidUserName");
}
}
private string notification;
public string Notification
{
get
{
return notification;
}
set
{
if (notification != value)
{
notification = value;
RaisePropertyChanged("Notification");
}
}
}
private string username;
public string Username
{
get
{
return username;
}
set
{
if (username != value)
{
username = value;
NotVaidUserName = VaidateUserName(username);
RaisePropertyChanged("Username");
}
}
}
public bool VaidateUserName(string strUsername)
{
bool bValidUserName = false;
if (!string.IsNullOrWhiteSpace(strUsername))
{
if (new Regex(#"^[a-zA-Z0-9]*$").IsMatch(strUsername))
{
bValidUserName = true;
if (strUsername.Length > 7)
{
Notification = "Max allowed key length is 6";
bValidUserName = false;
}
}
else
{
Notification = "No special characters allowed";
}
}
return bValidUserName;
}
public string validateCredentials(string Username, string Password)
{
return "Valid Credentials";
}
private void RaisePropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
class BoolToVisibleOrHidden : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
object returnvalue = new object();
returnvalue = (bool)value ? Visibility.Visible : parameter != null ? Visibility.Collapsed : Visibility.Hidden;
return returnvalue;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return (Visibility)value == Visibility.Visible;
}
}
}
Many thanks in advance.
Durga
The reason your code is not working is that you have not set the data context for view. I like to set the data context in the xaml as it will give you auto-complete in VS for your binding instruction. On the root node, add the attribute
DataContext="{Binding RelativeSource={RelativeSource Self}}"
This will set the window itself as your data context, allowing your command to work. However, the binding expressions on the textboxes will fail. You have split your ViewModel logic over the view and the ViewModel.
I would move you code completely into the ViewModel and use
DataContext="{Binding RelativeSource={RelativeSource Self}, Path=ViewModel}"
as the data context. Then the only code you have in the code behind of the view would be something along the lines of
namespace ExcelUtility
{
public partial class MainWindow : Window
{
private ViewModel viewModel;
public MainWindow()
{
InitializeComponent();
}
public ViewModel ViewModel { get { return viewModel ?? (viewModel = new ViewModel()); } }
}
}
Once your bindings are working, you will not have to set the UserName or Password (as you do in the navigatePageCanExecute and navigatePageExecuted methods) in the view model as the binding will set it for you.
I am not sure about your CommandsLibrary. You didn't include it in the example.
This should give you a start to figure out the rest.
I hope it helps
What I am trying to achieve is essentially in-place editing of a databound object inside an ItemsControl in wpf.
my ItemsControl is a horizontal WrapPanel containing multiple instances of a usercontrol (NameControl) which displays as a little pink Glyph with a person's name. It looks like this
With a popup I am able to show an editor for this "Name" (Other properties of the bound object things like Address,Gender etc.) and this works absoluttely fine. My XAML at this point would be along the lines of
<Style x:Key="NamesStyle" TargetType="{x:Type ItemsControl}">
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="ItemTemplate">
<Setter.Value>
<DataTemplate>
<StackPanel>
<Button Command="{Binding EditName}" BorderThickness="0" Background="Transparent" Panel.ZIndex="1">
<widgets:NameControl />
</Button>
<Popup IsOpen="{Binding IsEditMode}"
PlacementTarget="{Binding ElementName=button}"
Margin="0 5 0 0" Placement="Relative" AllowsTransparency="True" >
<Border Background="White" BorderBrush="DarkOrchid" BorderThickness="1,1,1,1" CornerRadius="5,5,5,5"
Panel.ZIndex="100">
<Grid ShowGridLines="False" Margin="5" Background="White" Width="300">
<!-- Grid Content - just editor fields/button etc -->
</Grid>
</Border>
</Popup>
</StackPanel>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
Giving an output when I click a Name looking like
With this look im quite happy (apart from my awful choice of colours!!) except that the popup does not move with the widow (resize/minimize/maximize) and that popup is above everything even other windows.
So one way to solve part of that is to "attach" or lock the popup position to the element. I have not found a good/easy/xaml way to do that. Ive come across a few code-based solutions but im not sure I like that. It just has a bit of a smell about it.
Another solution ive tried to achieve is to ditch the popup and try to emulate the behaviour of a layer/panel that sits above the other names but is position over (or below, im not fussy) the associated name control.
Ive tried a few different things, mainly around setting Panel.ZIndex on controls within a PanelControl (The Grid, the WrapPanel, a DockPanel on the very top of my MainWindow) with little success. I have implemented a simple BoolToVisibilityConverter to bind my editor Grid's Visibility property to my IsEditMode view model property and that works fine, but I cant for the life of me arrange my elements in the ItemsControl to show the editor grid over the names.
To do what is described above I simply commented out the Popup and added the following binding to the Border which contains the editor grid Visibility="{Binding IsEditMode, Converter={StaticResource boolToVisibility}}".
All that does is this:
It just shows the popup under the name but not over the others.
Any help? What am I doing wrong?
Sounds like a job for the AdornerLayer to me.
My implementation will just display one 'popup' at a time, and you can hide it by clicking the button another time. But you could also add a small close button to the ContactAdorner, or stick with your OK button, or fill the AdornerLayer behind the ContactAdorner with an element that IsHitTestVisible and reacts on click by hiding the open Adorner (so clicking anywhere outside closes the popup).
Edit: Added the small close button at your request. Changes in ContactAdorner and the ContactDetailsTemplate.
Another thing that you might want to add is repositioning of the adorner once it is clipped from the bottom (I only check for clipping from the right).
Xaml:
<UserControl x:Class="WpfApplication1.ItemsControlAdorner"
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:i="http://schemas.microsoft.com/expression/2010/interactivity"
mc:Ignorable="d"
xmlns:local="clr-namespace:WpfApplication1"
d:DesignHeight="300" d:DesignWidth="300">
<UserControl.DataContext>
<local:ViewModel />
</UserControl.DataContext>
<UserControl.Resources>
<local:EnumToBooleanConverter x:Key="EnumToBooleanConverter" />
<!-- Template for the Adorner -->
<DataTemplate x:Key="ContactDetailsTemplate" DataType="{x:Type local:MyContact}" >
<Border Background="#BBFFFFFF" BorderBrush="DarkOrchid" BorderThickness="1" CornerRadius="5" TextElement.Foreground="DarkOrchid" >
<Grid Margin="5" Width="300">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Text="Full name" />
<TextBox Grid.Row="1" Text="{Binding FullName, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Grid.Row="2" Text="Address" />
<TextBox Grid.Row="3" Grid.ColumnSpan="2" Text="{Binding Address}" />
<TextBlock Grid.Column="1" Text="Gender" />
<StackPanel Orientation="Horizontal" Grid.Column="1" Grid.Row="1" >
<RadioButton Content="Male" IsChecked="{Binding Gender, Converter={StaticResource EnumToBooleanConverter}, ConverterParameter={x:Static local:Gender.Male}}" />
<RadioButton Content="Female" IsChecked="{Binding Gender, Converter={StaticResource EnumToBooleanConverter}, ConverterParameter={x:Static local:Gender.Female}}" />
</StackPanel>
<Button x:Name="PART_CloseButton" Grid.Column="2" Height="16">
<Button.Template>
<ControlTemplate>
<Border Background="#01FFFFFF" Padding="3" >
<Path Stretch="Uniform" ClipToBounds="True" Stroke="DarkOrchid" StrokeThickness="2.5" Data="M 85.364473,6.9977109 6.0640998,86.29808 6.5333398,85.76586 M 6.9926698,7.4977169 86.293043,86.79809 85.760823,86.32885" />
</Border>
</ControlTemplate>
</Button.Template>
</Button>
</Grid>
</Border>
</DataTemplate>
<!-- Button/Item style -->
<Style x:Key="ButtonStyle1" TargetType="{x:Type Button}" >
<Setter Property="Foreground" Value="White" />
<Setter Property="FontFamily" Value="Times New Roman" />
<Setter Property="Background" Value="#CC99E6" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="MinHeight" Value="24" />
<Setter Property="Margin" Value="3,2" />
<Setter Property="Padding" Value="3,2" />
<Setter Property="Border.CornerRadius" Value="8" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border CornerRadius="{TemplateBinding Border.CornerRadius}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Padding="{TemplateBinding Padding}" Margin="{TemplateBinding Margin}" >
<ContentPresenter VerticalAlignment="Center" HorizontalAlignment="Center" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- ItemsControl style -->
<Style x:Key="NamesStyle" TargetType="{x:Type ItemsControl}">
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="ItemTemplate">
<Setter.Value>
<DataTemplate>
<Button x:Name="button" Style="{StaticResource ButtonStyle1}" Content="{Binding FullName}" >
<i:Interaction.Behaviors>
<local:ShowAdornerBehavior DataTemplate="{StaticResource ContactDetailsTemplate}" />
</i:Interaction.Behaviors>
</Button>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</UserControl.Resources>
<Grid>
<ItemsControl ItemsSource="{Binding MyContacts}" Style="{StaticResource NamesStyle}" />
</Grid>
</UserControl>
ShowAdornerBehavior, ContactAdorner, EnumToBooleanConverter:
using System.Windows;
using System.Linq;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Interactivity;
using System.Windows.Media;
using System.Windows.Data;
using System;
namespace WpfApplication1
{
public class ShowAdornerBehavior : Behavior<Button>
{
public DataTemplate DataTemplate { get; set; }
protected override void OnAttached()
{
this.AssociatedObject.Click += AssociatedObject_Click;
base.OnAttached();
}
void AssociatedObject_Click(object sender, RoutedEventArgs e)
{
var adornerLayer = AdornerLayer.GetAdornerLayer(this.AssociatedObject);
var contactAdorner = new ContactAdorner(this.AssociatedObject, adornerLayer, this.AssociatedObject.DataContext, this.DataTemplate);
}
}
public class ContactAdorner : Adorner
{
private ContentPresenter _contentPresenter;
private AdornerLayer _adornerLayer;
private static Button _btn;
private VisualCollection _visualChildren;
private double _marginRight = 5;
private double _adornerDistance = 5;
private PointCollection _points;
private static ContactAdorner _currentInstance;
public ContactAdorner(Button adornedElement, AdornerLayer adornerLayer, object data, DataTemplate dataTemplate)
: base(adornedElement)
{
if (_currentInstance != null)
_currentInstance.Hide(); // hides other adorners of the same type
if (_btn != null && _btn == adornedElement)
{
_currentInstance.Hide(); // hides the adorner of this button (toggle)
_btn = null;
}
else
{
_adornerLayer = adornerLayer;
_btn = adornedElement;
// adjust position if sizes change
_adornerLayer.SizeChanged += (s, e) => { UpdatePosition(); };
_btn.SizeChanged += (s, e) => { UpdatePosition(); };
_contentPresenter = new ContentPresenter() { Content = data, ContentTemplate = dataTemplate };
// apply template explicitly: http://stackoverflow.com/questions/5679648/why-would-this-contenttemplate-findname-throw-an-invalidoperationexception-on
_contentPresenter.ApplyTemplate();
// get close button from datatemplate
Button closeBtn = _contentPresenter.ContentTemplate.FindName("PART_CloseButton", _contentPresenter) as Button;
if (closeBtn != null)
closeBtn.Click += (s, e) => { this.Hide(); _btn = null; };
_visualChildren = new VisualCollection(this); // this is needed for user interaction with the adorner layer
_visualChildren.Add(_contentPresenter);
_adornerLayer.Add(this);
_currentInstance = this;
UpdatePosition(); // position adorner
}
}
/// <summary>
/// Positioning is a bit fiddly.
/// Also, this method is only dealing with the right clip, not yet with the bottom clip.
/// </summary>
private void UpdatePosition()
{
double marginLeft = 0;
_contentPresenter.Margin = new Thickness(marginLeft, 0, _marginRight, 0); // "reset" margin to get a good measure pass
_contentPresenter.Measure(_adornerLayer.RenderSize); // measure the contentpresenter to get a DesiredSize
var contentRect = new Rect(_contentPresenter.DesiredSize);
double right = _btn.TranslatePoint(new Point(contentRect.Width, 0), _adornerLayer).X; // this does not work with the contentpresenter, so use _adornedElement
if (right > _adornerLayer.ActualWidth) // if adorner is clipped by right window border, move it to the left
marginLeft = _adornerLayer.ActualWidth - right;
_contentPresenter.Margin = new Thickness(marginLeft, _btn.ActualHeight + _adornerDistance, _marginRight, 0); // position adorner
DrawArrow();
}
private void DrawArrow()
{
Point bottomMiddleButton = new Point(_btn.ActualWidth / 2, _btn.ActualHeight - _btn.Margin.Bottom);
Point topLeftAdorner = new Point(_btn.ActualWidth / 2 - 10, _contentPresenter.Margin.Top);
Point topRightAdorner = new Point(_btn.ActualWidth / 2 + 10, _contentPresenter.Margin.Top);
PointCollection points = new PointCollection();
points.Add(bottomMiddleButton);
points.Add(topLeftAdorner);
points.Add(topRightAdorner);
_points = points; // actual drawing executed in OnRender
}
protected override void OnRender(DrawingContext drawingContext)
{
// Drawing the arrow
StreamGeometry streamGeometry = new StreamGeometry();
using (StreamGeometryContext geometryContext = streamGeometry.Open())
{
if (_points != null && _points.Any())
{
geometryContext.BeginFigure(_points[0], true, true);
geometryContext.PolyLineTo(_points.Where(p => _points.IndexOf(p) > 0).ToList(), true, true);
}
}
// Draw the polygon visual
drawingContext.DrawGeometry(Brushes.DarkOrchid, new Pen(_btn.Background, 0.5), streamGeometry);
base.OnRender(drawingContext);
}
private void Hide()
{
_adornerLayer.Remove(this);
}
protected override Size MeasureOverride(Size constraint)
{
_contentPresenter.Measure(constraint);
return _contentPresenter.DesiredSize;
}
protected override Size ArrangeOverride(Size finalSize)
{
_contentPresenter.Arrange(new Rect(finalSize));
return finalSize;
}
protected override Visual GetVisualChild(int index)
{
return _visualChildren[index];
}
protected override int VisualChildrenCount
{
get { return _visualChildren.Count; }
}
}
// http://stackoverflow.com/questions/397556/how-to-bind-radiobuttons-to-an-enum
public class EnumToBooleanConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return value.Equals(parameter);
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return value.Equals(true) ? parameter : Binding.DoNothing;
}
}
}
ViewModel, MyContact:
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows.Input;
namespace WpfApplication1
{
public class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
private ObservableCollection<MyContact> _myContacts = new ObservableCollection<MyContact>();
public ObservableCollection<MyContact> MyContacts { get { return _myContacts; } set { _myContacts = value; OnPropertyChanged("MyContacts"); } }
public ViewModel()
{
MyContacts = new ObservableCollection<MyContact>()
{
new MyContact() { FullName = "Sigmund Freud", Gender = Gender.Male },
new MyContact() { FullName = "Abraham Lincoln", Gender = Gender.Male },
new MyContact() { FullName = "Joan Of Arc", Gender = Gender.Female },
new MyContact() { FullName = "Bob the Khann", Gender = Gender.Male, Address = "Mongolia" },
new MyContact() { FullName = "Freddy Mercury", Gender = Gender.Male },
new MyContact() { FullName = "Giordano Bruno", Gender = Gender.Male },
new MyContact() { FullName = "Socrates", Gender = Gender.Male },
new MyContact() { FullName = "Marie Curie", Gender = Gender.Female }
};
}
}
public class MyContact : INotifyPropertyChanged
{
private string _fullName;
public string FullName { get { return _fullName; } set { _fullName = value; OnPropertyChanged("FullName"); } }
private string _address;
public string Address { get { return _address; } set { _address = value; OnPropertyChanged("Address"); } }
private Gender _gender;
public Gender Gender { get { return _gender; } set { _gender = value; OnPropertyChanged("Gender"); } }
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public enum Gender
{
Male,
Female
}
Personally I hate WPF's built in Popup control for exactly those reasons, and my workaround is to use a Custom Popup UserControl
Basically I'll put the Popup in a panel that allows it's children to overlap, such as a Grid or a Canvas, and position it on top of whatever content it's supposed to be on top of.
It includes DependencyProperties to specify it's parent panel and if it's open or not, and is part of the normal VisualTree so it will move around with your Window and act the same way any regular UI element would.
Typical usage would look like this:
<Grid x:Name="ParentPanel">
<ItemsControl ... />
<local:PopupPanel Content="{Binding PopupContent}"
local:PopupPanel.PopupParent="{Binding ElementName=ParentPanel}"
local:PopupPanel.IsPopupVisible="{Binding IsPopupVisible}" />
</Grid>
The code for the UserControl can be found on my blog along with a downloadable example of its use, but I'll also post a copy of it here.
The XAML for the UserControl is:
<UserControl x:Class="PopupPanelSample.PopupPanel"
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:local="clr-namespace:PopupPanelSample"
FocusManager.IsFocusScope="True"
>
<UserControl.Template>
<ControlTemplate TargetType="{x:Type local:PopupPanel}">
<ControlTemplate.Resources>
<!-- Converter to get Popup Positioning -->
<local:ValueDividedByParameterConverter x:Key="ValueDividedByParameterConverter" />
<!-- Popup Visibility -->
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
<Style x:Key="PopupPanelContentStyle" TargetType="{x:Type Grid}">
<Setter Property="Grid.Visibility" Value="{Binding Path=IsPopupVisible,
RelativeSource={RelativeSource AncestorType={x:Type local:PopupPanel}},
Converter={StaticResource BooleanToVisibilityConverter}}"/>
</Style>
</ControlTemplate.Resources>
<Grid x:Name="PopupPanelContent" Style="{StaticResource PopupPanelContentStyle}">
<Grid.Resources>
<!-- Storyboard to show Content -->
<Storyboard x:Key="ShowEditPanelStoryboard" SpeedRatio="5">
<DoubleAnimation
Storyboard.TargetName="PopupPanelContent"
Storyboard.TargetProperty="RenderTransform.(ScaleTransform.ScaleX)"
From="0.00" To="1.00" Duration="00:00:01"
/>
<DoubleAnimation
Storyboard.TargetName="PopupPanelContent"
Storyboard.TargetProperty="RenderTransform.(ScaleTransform.ScaleY)"
From="0.00" To="1.00" Duration="00:00:01"
/>
</Storyboard>
</Grid.Resources>
<!-- Setting up RenderTransform for Popup Animation -->
<Grid.RenderTransform>
<ScaleTransform
CenterX="{Binding Path=PopupParent.ActualWidth, Converter={StaticResource ValueDividedByParameterConverter}, ConverterParameter=2, RelativeSource={RelativeSource AncestorType={x:Type local:PopupPanel}}}"
CenterY="{Binding Path=PopupParent.ActualHeight, Converter={StaticResource ValueDividedByParameterConverter}, ConverterParameter=2, RelativeSource={RelativeSource AncestorType={x:Type local:PopupPanel}}}"
/>
</Grid.RenderTransform>
<!-- Grayscale background & prevents mouse input -->
<Rectangle
Fill="Gray"
Opacity="{Binding Path=BackgroundOpacity, RelativeSource={RelativeSource AncestorType={x:Type local:PopupPanel}}}"
Height="{Binding RelativeSource={RelativeSource AncestorType={x:Type local:PopupPanel}}, Path=Height}"
Width="{Binding RelativeSource={RelativeSource AncestorType={x:Type local:PopupPanel}}, Path=Width}"
/>
<!-- Popup Content -->
<ContentControl x:Name="PopupContentControl"
KeyboardNavigation.TabNavigation="Cycle"
PreviewKeyDown="PopupPanel_PreviewKeyDown"
PreviewLostKeyboardFocus="PopupPanel_LostFocus"
IsVisibleChanged="PopupPanel_IsVisibleChanged"
HorizontalAlignment="Center" VerticalAlignment="Center"
>
<ContentPresenter Content="{TemplateBinding Content}" />
</ContentControl>
</Grid>
</ControlTemplate>
</UserControl.Template>
</UserControl>
And the code-behind the UserControl looks like this:
using System;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Threading;
namespace PopupPanelSample
{
/// <summary>
/// Panel for handling Popups:
/// - Control with name PART_DefaultFocusControl will have default focus
/// - Can define PopupParent to determine if this popup should be hosted in a parent panel or not
/// - Can define the property EnterKeyCommand to specifify what command to run when the Enter key is pressed
/// - Can define the property EscapeKeyCommand to specify what command to run when the Escape key is pressed
/// - Can define BackgroundOpacity to specify how opaque the background will be. Value is between 0 and 1.
/// </summary>
public partial class PopupPanel : UserControl
{
#region Fields
bool _isLoading = false; // Flag to tell identify when DataContext changes
private UIElement _lastFocusControl; // Last control that had focus when popup visibility changes, but isn't closed
#endregion // Fields
#region Constructors
public PopupPanel()
{
InitializeComponent();
this.DataContextChanged += Popup_DataContextChanged;
// Register a PropertyChanged event on IsPopupVisible
DependencyPropertyDescriptor dpd = DependencyPropertyDescriptor.FromProperty(PopupPanel.IsPopupVisibleProperty, typeof(PopupPanel));
if (dpd != null) dpd.AddValueChanged(this, delegate { IsPopupVisible_Changed(); });
dpd = DependencyPropertyDescriptor.FromProperty(PopupPanel.ContentProperty, typeof(PopupPanel));
if (dpd != null) dpd.AddValueChanged(this, delegate { Content_Changed(); });
}
#endregion // Constructors
#region Events
#region Property Change Events
// When DataContext changes
private void Popup_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
DisableAnimationWhileLoading();
}
// When Content Property changes
private void Content_Changed()
{
DisableAnimationWhileLoading();
}
// Sets an IsLoading flag so storyboard doesn't run while loading
private void DisableAnimationWhileLoading()
{
_isLoading = true;
this.Dispatcher.BeginInvoke(DispatcherPriority.Render,
new Action(delegate() { _isLoading = false; }));
}
// Run storyboard when IsPopupVisible property changes to true
private void IsPopupVisible_Changed()
{
bool isShown = GetIsPopupVisible(this);
if (isShown && !_isLoading)
{
FrameworkElement panel = FindChild<FrameworkElement>(this, "PopupPanelContent");
if (panel != null)
{
// Run Storyboard
Storyboard animation = (Storyboard)panel.FindResource("ShowEditPanelStoryboard");
animation.Begin();
}
}
// When hiding popup, clear the LastFocusControl
if (!isShown)
{
_lastFocusControl = null;
}
}
#endregion // Change Events
#region Popup Events
// When visibility is changed, set the default focus
void PopupPanel_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if ((bool)e.NewValue)
{
ContentControl popupControl = FindChild<ContentControl>(this, "PopupContentControl");
this.Dispatcher.BeginInvoke(DispatcherPriority.Render,
new Action(delegate()
{
// Verify object really is visible because sometimes it's not once we switch to Render
if (!GetIsPopupVisible(this))
{
return;
}
if (_lastFocusControl != null && _lastFocusControl.Focusable)
{
_lastFocusControl.Focus();
}
else
{
_lastFocusControl = FindChild<UIElement>(popupControl, "PART_DefaultFocusControl") as UIElement;
// If we can find the part named PART_DefaultFocusControl, set focus to it
if (_lastFocusControl != null && _lastFocusControl.Focusable)
{
_lastFocusControl.Focus();
}
else
{
_lastFocusControl = FindFirstFocusableChild(popupControl);
// If no DefaultFocusControl found, try and set focus to the first focusable element found in popup
if (_lastFocusControl != null)
{
_lastFocusControl.Focus();
}
else
{
// Just give the Popup UserControl focus so it can handle keyboard input
popupControl.Focus();
}
}
}
}
)
);
}
}
// When popup loses focus but isn't hidden, store the last element that had focus so we can put it back later
void PopupPanel_LostFocus(object sender, RoutedEventArgs e)
{
DependencyObject focusScope = FocusManager.GetFocusScope(this);
_lastFocusControl = FocusManager.GetFocusedElement(focusScope) as UIElement;
}
// Keyboard Events
private void PopupPanel_PreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Escape)
{
PopupPanel popup = FindAncester<PopupPanel>((DependencyObject)sender);
ICommand cmd = GetPopupEscapeKeyCommand(popup);
if (cmd != null && cmd.CanExecute(null))
{
cmd.Execute(null);
e.Handled = true;
}
else
{
// By default the Escape Key closes the popup when pressed
var expression = this.GetBindingExpression(PopupPanel.IsPopupVisibleProperty);
var dataType = expression.DataItem.GetType();
dataType.GetProperties().Single(x => x.Name == expression.ParentBinding.Path.Path)
.SetValue(expression.DataItem, false, null);
}
}
else if (e.Key == Key.Enter)
{
// Don't want to run Enter command if focus is in a TextBox with AcceptsReturn = True
if (!(e.KeyboardDevice.FocusedElement is TextBox &&
(e.KeyboardDevice.FocusedElement as TextBox).AcceptsReturn == true))
{
PopupPanel popup = FindAncester<PopupPanel>((DependencyObject)sender);
ICommand cmd = GetPopupEnterKeyCommand(popup);
if (cmd != null && cmd.CanExecute(null))
{
cmd.Execute(null);
e.Handled = true;
}
}
}
}
#endregion // Popup Events
#endregion // Events
#region Dependency Properties
// Parent for Popup
#region PopupParent
public static readonly DependencyProperty PopupParentProperty =
DependencyProperty.Register("PopupParent", typeof(FrameworkElement),
typeof(PopupPanel), new PropertyMetadata(null, null, CoercePopupParent));
private static object CoercePopupParent(DependencyObject obj, object value)
{
// If PopupParent is null, return the Window object
return (value ?? FindAncester<Window>(obj));
}
public FrameworkElement PopupParent
{
get { return (FrameworkElement)this.GetValue(PopupParentProperty); }
set { this.SetValue(PopupParentProperty, value); }
}
// Providing Get/Set methods makes them show up in the XAML designer
public static FrameworkElement GetPopupParent(DependencyObject obj)
{
return (FrameworkElement)obj.GetValue(PopupParentProperty);
}
public static void SetPopupParent(DependencyObject obj, FrameworkElement value)
{
obj.SetValue(PopupParentProperty, value);
}
#endregion
// Popup Visibility - If popup is shown or not
#region IsPopupVisibleProperty
public static readonly DependencyProperty IsPopupVisibleProperty =
DependencyProperty.Register("IsPopupVisible", typeof(bool),
typeof(PopupPanel), new PropertyMetadata(false, null));
public static bool GetIsPopupVisible(DependencyObject obj)
{
return (bool)obj.GetValue(IsPopupVisibleProperty);
}
public static void SetIsPopupVisible(DependencyObject obj, bool value)
{
obj.SetValue(IsPopupVisibleProperty, value);
}
#endregion // IsPopupVisibleProperty
// Transparency level for the background filler outside the popup
#region BackgroundOpacityProperty
public static readonly DependencyProperty BackgroundOpacityProperty =
DependencyProperty.Register("BackgroundOpacity", typeof(double),
typeof(PopupPanel), new PropertyMetadata(.5, null));
public static double GetBackgroundOpacity(DependencyObject obj)
{
return (double)obj.GetValue(BackgroundOpacityProperty);
}
public static void SetBackgroundOpacity(DependencyObject obj, double value)
{
obj.SetValue(BackgroundOpacityProperty, value);
}
#endregion ShowBackgroundProperty
// Command to execute when Enter key is pressed
#region PopupEnterKeyCommandProperty
public static readonly DependencyProperty PopupEnterKeyCommandProperty =
DependencyProperty.RegisterAttached("PopupEnterKeyCommand", typeof(ICommand),
typeof(PopupPanel), new PropertyMetadata(null, null));
public static ICommand GetPopupEnterKeyCommand(DependencyObject obj)
{
return (ICommand)obj.GetValue(PopupEnterKeyCommandProperty);
}
public static void SetPopupEnterKeyCommand(DependencyObject obj, ICommand value)
{
obj.SetValue(PopupEnterKeyCommandProperty, value);
}
#endregion PopupEnterKeyCommandProperty
// Command to execute when Enter key is pressed
#region PopupEscapeKeyCommandProperty
public static readonly DependencyProperty PopupEscapeKeyCommandProperty =
DependencyProperty.RegisterAttached("PopupEscapeKeyCommand", typeof(ICommand),
typeof(PopupPanel), new PropertyMetadata(null, null));
public static ICommand GetPopupEscapeKeyCommand(DependencyObject obj)
{
return (ICommand)obj.GetValue(PopupEscapeKeyCommandProperty);
}
public static void SetPopupEscapeKeyCommand(DependencyObject obj, ICommand value)
{
obj.SetValue(PopupEscapeKeyCommandProperty, value);
}
#endregion PopupEscapeKeyCommandProperty
#endregion Dependency Properties
#region Visual Tree Helpers
public static UIElement FindFirstFocusableChild(DependencyObject parent)
{
// Confirm parent is valid.
if (parent == null) return null;
UIElement foundChild = null;
int childrenCount = VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < childrenCount; i++)
{
UIElement child = VisualTreeHelper.GetChild(parent, i) as UIElement;
// This is returning me things like ContentControls, so for now filtering to buttons/textboxes only
if (child != null && child.Focusable && child.IsVisible)
{
foundChild = child;
break;
}
// recursively drill down the tree
foundChild = FindFirstFocusableChild(child);
// If the child is found, break so we do not overwrite the found child.
if (foundChild != null) break;
}
return foundChild;
}
public static T FindAncester<T>(DependencyObject current)
where T : DependencyObject
{
// Need this call to avoid returning current object if it is the same type as parent we are looking for
current = VisualTreeHelper.GetParent(current);
while (current != null)
{
if (current is T)
{
return (T)current;
}
current = VisualTreeHelper.GetParent(current);
};
return null;
}
/// <summary>
/// Looks for a child control within a parent by name
/// </summary>
public static T FindChild<T>(DependencyObject parent, string childName)
where T : DependencyObject
{
// Confirm parent and childName are valid.
if (parent == null) return null;
T foundChild = null;
int childrenCount = VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < childrenCount; i++)
{
var child = VisualTreeHelper.GetChild(parent, i);
// If the child is not of the request child type child
T childType = child as T;
if (childType == null)
{
// recursively drill down the tree
foundChild = FindChild<T>(child, childName);
// If the child is found, break so we do not overwrite the found child.
if (foundChild != null) break;
}
else if (!string.IsNullOrEmpty(childName))
{
var frameworkElement = child as FrameworkElement;
// If the child's name is set for search
if (frameworkElement != null && frameworkElement.Name == childName)
{
// if the child's name is of the request name
foundChild = (T)child;
break;
}
else
{
// recursively drill down the tree
foundChild = FindChild<T>(child, childName);
// If the child is found, break so we do not overwrite the found child.
if (foundChild != null) break;
}
}
else
{
// child element found.
foundChild = (T)child;
break;
}
}
return foundChild;
}
#endregion
}
// Converter for Popup positioning
public class ValueDividedByParameterConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
double n, d;
if (double.TryParse(value.ToString(), out n)
&& double.TryParse(parameter.ToString(), out d)
&& d != 0)
{
return n / d;
}
return 0;
} public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
Here is the problem:
I want to sort a ListView when it is first loaded.
I have implemented the functionality where in the List View can be sorted if the Header columns
in the ListView are clicked.
I unable to find a suitable event which I can use to call my sort function.
I tried using OnInitialized of the UserControl and Loaded events but it seems the List View is
not populated when I call these functions.
I tried GotFocus of ListView. It works but then I have to click on the window to get the sorting done.
I want the sorting to be done as soon as the ListView is loaded.
I am using XML data binding with the List View.
The ListView is part of a UserControl. The User Control is hosted in a MMC app.
Please let me know if you need any other information.
public class SortableGridViewColumn : GridViewColumn
{
public string SortPropertyName
{
get { return (string)GetValue(SortPropertyNameProperty); }
set { SetValue(SortPropertyNameProperty, value); }
}
// Using a DependencyProperty as the backing store for SortPropertyName. This enables animation, styling, binding, etc...
public static readonly DependencyProperty SortPropertyNameProperty =
DependencyProperty.Register("SortPropertyName", typeof(string),
typeof(SortableGridViewColumn), new UIPropertyMetadata(""));
public bool IsDefaultSortColumn
{
get { return (bool)GetValue(IsDefaultSortColumnProperty); }
set { SetValue(IsDefaultSortColumnProperty, value); }
}
public static readonly DependencyProperty IsDefaultSortColumnProperty =
DependencyProperty.Register("IsDefaultSortColumn", typeof(bool),
typeof(SortableGridViewColumn), new UIPropertyMetadata(false));
}
public class SortableListView : ListView
{
public SortableListView()
{
}
SortableGridViewColumn lastSortedOnColumn = null;
ListSortDirection lastDirection = ListSortDirection.Ascending;
public void Sort(string sortBy, ListSortDirection direction)
{
ICollectionView dataView = CollectionViewSource.GetDefaultView
(this.ItemsSource);
//Check if dataView isn't null
if (dataView != null)
{
dataView.SortDescriptions.Clear();
SortDescription sd1 = new SortDescription("#isenabled", direction);
dataView.SortDescriptions.Add(sd1);
SortDescription sd = new SortDescription(sortBy, direction);
dataView.SortDescriptions.Add(sd);
dataView.Refresh();
}
}
private void GridViewColumnHeaderClickedHandler(object sender, RoutedEventArgs e)
{
GridViewColumnHeader headerClicked = e.OriginalSource as GridViewColumnHeader;
if (headerClicked != null &&
headerClicked.Role != GridViewColumnHeaderRole.Padding)
{
// attempt to cast to the sortableGridViewColumn object.
SortableGridViewColumn sortableGridViewColumn = (headerClicked.Column) as SortableGridViewColumn;
// ensure that the column header is the correct type and a sort property has been set.
if (sortableGridViewColumn != null && !String.IsNullOrEmpty(sortableGridViewColumn.SortPropertyName))
{
ListSortDirection direction;
bool newSortColumn = false;
// determine if this is a new sort, or a switch in sort direction.
if (lastSortedOnColumn == null
|| String.IsNullOrEmpty(lastSortedOnColumn.SortPropertyName)
|| !String.Equals(sortableGridViewColumn.SortPropertyName, lastSortedOnColumn.SortPropertyName, StringComparison.InvariantCultureIgnoreCase))
{
newSortColumn = true;
direction = ListSortDirection.Ascending;
}
else
{
if (lastDirection == ListSortDirection.Ascending)
{
direction = ListSortDirection.Descending;
}
else
{
direction = ListSortDirection.Ascending;
}
}
// get the sort property name from the column's information.
string sortPropertyName = sortableGridViewColumn.SortPropertyName;
// Sort the data.
Sort(sortPropertyName, direction);
lastSortedOnColumn = sortableGridViewColumn;
lastDirection = direction;
}
}
}
protected override void OnInitialized(EventArgs e)
{
base.OnInitialized(e);
// add the event handler to the GridViewColumnHeader. This strongly ties this ListView to a GridView.
this.AddHandler(GridViewColumnHeader.ClickEvent, new RoutedEventHandler(GridViewColumnHeaderClickedHandler));
// cast the ListView's View to a GridView
GridView gridView = this.View as GridView;
if (gridView != null)
{
// determine which column is marked as IsDefaultSortColumn. Stops on the first column marked this way.1
SortableGridViewColumn sortableGridViewColumn = null;
foreach (GridViewColumn gridViewColumn in gridView.Columns)
{
sortableGridViewColumn = gridViewColumn as SortableGridViewColumn;
if (sortableGridViewColumn != null)
{
if (sortableGridViewColumn.IsDefaultSortColumn)
{
break;
}
sortableGridViewColumn = null;
}
}
// if the default sort column is defined, sort the data
if (sortableGridViewColumn != null)
{
lastSortedOnColumn = sortableGridViewColumn;
Sort(sortableGridViewColumn.SortPropertyName, ListSortDirection.Ascending);
}
}
}
}
The XAML is as shown below:
**<local:SortableListView x:Name="ListViewControl" Grid.Row="0" ItemContainerStyle="{DynamicResource StretchedContainerStyle}"
ItemTemplateSelector="{DynamicResource myControlTemplateSelector}"
IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding Source={StaticResource dataProvider},
XPath=//CONFIGURATION}">
<ListView.View >
<GridView >
<local:SortableGridViewColumn Header="ID" HeaderContainerStyle="{StaticResource CustomHeaderStyle}"
DisplayMemberBinding="{Binding XPath=./#id}"
IsDefaultSortColumn="True"
SortPropertyName="#id"/>
<local:SortableGridViewColumn Header="VALUE" HeaderContainerStyle="{StaticResource CustomHeaderStyle}"
CellTemplateSelector="{DynamicResource myControlTemplateSelector}"
SortPropertyName="#value"/>
<local:SortableGridViewColumn Header="DATATYPE" HeaderContainerStyle="{StaticResource CustomHeaderStyle}"
DisplayMemberBinding="{Binding XPath=./#data_type}"
SortPropertyName="#data_type"/>
<local:SortableGridViewColumn Header="DESCRIPTION" HeaderContainerStyle="{StaticResource CustomHeaderStyle}"
DisplayMemberBinding="{Binding XPath=./#description}"
SortPropertyName="#description"
Width="{Binding ElementName=ListViewControl, Path=ActualWidth}"/>
</GridView>
</ListView.View>
</local:SortableListView>**
<StackPanel Grid.Row="1">
<Button Grid.Row="1" HorizontalAlignment="Stretch" Height="34" HorizontalContentAlignment="Stretch" >
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Center" Orientation="Horizontal" FlowDirection="RightToLeft" Height="30">
<Button Grid.Row="1" Content ="Apply" Padding="0,0,0,0 " Margin="6,2,0,2" Name="btn_Apply" HorizontalAlignment="Right" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" Width="132" IsTabStop="True" Click="btn_ApplyClick" Height="24" />
</StackPanel >
</Button>
</StackPanel >
</Grid>
I finally was able to resolve it.
I had to use the Converter on ListView ItemSource. And then sort the List on Convert Function.
Here is the code below:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text;
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;
using System.Xml;
using System.Threading;
using System.Collections.ObjectModel;
namespace ...MiscellaneousCAESettings
{
/// <summary>
/// Interaction logic for ConfigDataView.xaml
/// </summary>
public partial class ConfigDataView : UserControl, IConfigDataViewControl
{
ConfigDataViewPresenter _presenter = null;
public static string _currDataType = "";
public static string _min = "" ;
public static string _max = "";
public string Min
{
get
{
return _min;
}
set
{
_min = value ;
}
}
public string Max
{
get
{
return _max;
}
set
{
_max = value;
}
}
public string CurrDataType
{
get
{
return _currDataType;
}
set
{
_currDataType = value;
}
}
public ConfigDataView()
{
InitializeComponent();
//To give the classic windows look
Uri uri = new Uri("PresentationFramework.Classic;V3.0.0.0;31bf3856ad364e35;component\\themes/classic.xaml", UriKind.Relative);
this.Resources.MergedDictionaries.Add(Application.LoadComponent(uri) as ResourceDictionary);
}
private void txtBoxGotFocus(object sender, RoutedEventArgs e)
{
Min = "" ;
Max = "" ;
TextBox txtbox = e.Source as TextBox;
this.ListViewControl.SelectedItem = txtbox.DataContext;
//index
int index = this.ListViewControl.Items.IndexOf(this.ListViewControl.SelectedItem);
System.ComponentModel.ICollectionView dataView = CollectionViewSource.GetDefaultView(this.ListViewControl.ItemsSource);
object stCurr = (dataView.CurrentPosition ) ;
//Check if the "data_type" attribute exists
if (((XmlElement)dataView.CurrentItem).Attributes["data_type"] != null)
{
CurrDataType = ((XmlElement)dataView.CurrentItem).Attributes["data_type"].Value;
}
//Check if the "min" attribute exists
if (((XmlElement)dataView.CurrentItem).Attributes["min"] != null)
{
Min = ((XmlElement)dataView.CurrentItem).Attributes["min"].Value;
}
//Check if the "min" attribute exists
if (((XmlElement)dataView.CurrentItem).Attributes["max"] != null)
{
Max = ((XmlElement)dataView.CurrentItem).Attributes["max"].Value;
}
}
#region IConfigDataViewControl Members
public void LoadRootConfigData(string xmlFileName, string xmlFileContent, string xmlXPath)
{
try
{
XmlDocument configFileDoc = new XmlDocument();
configFileDoc.LoadXml(xmlFileContent);
XmlDataProvider xmldp = (XmlDataProvider)this.TryFindResource("dataProvider");
xmldp.Document = configFileDoc;
if (string.IsNullOrEmpty(xmlXPath))
{
xmldp.XPath = #"//node()[1]/node()[#value]";
}
else
{
xmldp.XPath = xmlXPath;
}
Binding bnd = new Binding();
bnd.Source = xmldp;
bnd.Converter = new SortList();
ListViewControl.SetBinding(ItemsControl.ItemsSourceProperty, bnd);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
public void LoadCategoryConfigData(string xmlFile, string xmlFileContent, string CategoryNodeName)
{
try
{
XmlDocument configFileDoc = new XmlDocument();
configFileDoc.LoadXml(xmlFileContent);
XmlDataProvider xmldp = (XmlDataProvider)this.TryFindResource("dataProvider");
xmldp.Document = configFileDoc;
xmldp.XPath = #"//CONTEXT[#id='" + CategoryNodeName + #"']/CONFIGURATION";
Binding bnd = new Binding();
bnd.Source = xmldp;
bnd.Converter = new SortList();
ListViewControl.SetBinding(ItemsControl.ItemsSourceProperty, bnd);
}
catch(Exception ex)
{
MessageBox.Show(ex.Message);
}
}
public void AttachPresenter(ConfigDataViewPresenter cfgpresenter)
{
_presenter = cfgpresenter;
}
#endregion
private void btn_ApplyClick(object sender, RoutedEventArgs e)
{
XmlDataProvider odp = (XmlDataProvider)this.TryFindResource("dataProvider");
XmlDocument configFileDoc = new XmlDocument();
configFileDoc =odp.Document;
_presenter.Save(configFileDoc.InnerXml );
}
}
public class TextBoxMinMaxValidation : ValidationRule
{
public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
{
try
{
//Check for min max string length if it is a "Text" data type
if (ConfigDataView._currDataType.ToLower() == "text")
{
int minLength = Convert.ToInt32(ConfigDataView._min);
int maxLength = Convert.ToInt32(ConfigDataView._max);
int strLength = value.ToString().Length;
bool isValidLength = true;
isValidLength = ((strLength >= minLength) && (strLength <= maxLength));
if (!isValidLength)
{
return new ValidationResult(false, string.Format("The input String Length is out of range. The String Length should be between {0} to {1}", minLength, maxLength));
}
else
{
return new ValidationResult(true, null);
}
}
//Check for min max string length if it is a "Numeric" data type
if (ConfigDataView._currDataType.ToLower() != "numeric")
{
return new ValidationResult(true, null);
}
int min = Convert.ToInt32(ConfigDataView._min);
int max = Convert.ToInt32(ConfigDataView._max);
int res ;
bool isNumber = int.TryParse(value.ToString(), out res);
bool isValidRange = true;
if (!isNumber)
{
return new ValidationResult(false, "The input string is in incorrect format. Should be a Number.");
}
isValidRange = ((res >= min) && (res <= max));
if (!isValidRange)
{
return new ValidationResult(false, string.Format("The input integer is out of range. The number should be between {0} to {1}", min, max));
}
}
catch
{
}
return new ValidationResult(true, null);
}
}
public class ControlTemplateSelector : DataTemplateSelector
{
public const String XML_TAG_DATATYPE = "data_type";
public const String DATATYPE_DROPDOWN = "Dropdown";
public const String DATATYPE_BOOLEAN = "Boolean";
public override DataTemplate SelectTemplate(object item,
DependencyObject container)
{
FrameworkElement window = (container as FrameworkElement);
try
{
XmlNode node = (XmlNode)item;
String dataType = "";
if (node.Attributes[XML_TAG_DATATYPE] != null)
{
dataType = (string)node.Attributes.GetNamedItem(XML_TAG_DATATYPE).Value;
}
if (dataType == DATATYPE_DROPDOWN)
{
return window.FindResource("dropDownTemplate") as DataTemplate;
}
if (dataType == DATATYPE_BOOLEAN)
{
return window.FindResource("booldropDownTemplate") as DataTemplate;
}
}
catch (Exception ex)
{
MessageBox.Show("Select template Exception" + ex.Message );
}
return window.FindResource("textTemplate") as DataTemplate;
}
}
public class boolConverter : IValueConverter
{
public const String XML_TAG_VALUE = "value";
public const String XML_TAG_ID = "id";
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
Boolean boolVal = false;
try
{
boolVal = System.Convert.ToBoolean(value);
}
catch
{
string strVal = value.ToString();
int iVal = int.Parse(strVal);
boolVal = System.Convert.ToBoolean(iVal);
}
if (boolVal == true)
{
return 1;
}
else
{
return 0;
}
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
Boolean boolVal = false;
try
{
boolVal = System.Convert.ToBoolean(value);
}
catch
{
string strVal = value.ToString();
int iVal = int.Parse(strVal);
boolVal = System.Convert.ToBoolean(iVal);
}
return boolVal;
}
}
public class SortableGridViewColumn : GridViewColumn
{
public string SortPropertyName
{
get { return (string)GetValue(SortPropertyNameProperty); }
set { SetValue(SortPropertyNameProperty, value); }
}
// Using a DependencyProperty as the backing store for SortPropertyName. This enables animation, styling, binding, etc...
public static readonly DependencyProperty SortPropertyNameProperty =
DependencyProperty.Register("SortPropertyName", typeof(string),
typeof(SortableGridViewColumn), new UIPropertyMetadata(""));
public bool IsDefaultSortColumn
{
get { return (bool)GetValue(IsDefaultSortColumnProperty); }
set { SetValue(IsDefaultSortColumnProperty, value); }
}
public static readonly DependencyProperty IsDefaultSortColumnProperty =
DependencyProperty.Register("IsDefaultSortColumn", typeof(bool),
typeof(SortableGridViewColumn), new UIPropertyMetadata(false));
}
public class SortableListView : ListView
{
public SortableListView()
{
// add the event handler to the GridViewColumnHeader. This strongly ties this ListView to a GridView.
this.AddHandler(GridViewColumnHeader.ClickEvent, new RoutedEventHandler(GridViewColumnHeaderClickedHandler));
}
SortableGridViewColumn lastSortedOnColumn = null;
ListSortDirection lastDirection = ListSortDirection.Ascending;
public void Sort(string sortBy, ListSortDirection direction)
{
ICollectionView dataView = CollectionViewSource.GetDefaultView
(this.ItemsSource);
//Check if dataView isn't null
if (dataView != null)
{
dataView.SortDescriptions.Clear();
SortDescription sd1 = new SortDescription("#isenabled", direction);
dataView.SortDescriptions.Add(sd1);
SortDescription sd = new SortDescription(sortBy, direction);
dataView.SortDescriptions.Add(sd);
dataView.Refresh();
}
}
private void GridViewColumnHeaderClickedHandler(object sender, RoutedEventArgs e)
{
GridViewColumnHeader headerClicked = e.OriginalSource as GridViewColumnHeader;
if (headerClicked != null &&
headerClicked.Role != GridViewColumnHeaderRole.Padding)
{
// attempt to cast to the sortableGridViewColumn object.
SortableGridViewColumn sortableGridViewColumn = (headerClicked.Column) as SortableGridViewColumn;
// ensure that the column header is the correct type and a sort property has been set.
if (sortableGridViewColumn != null && !String.IsNullOrEmpty(sortableGridViewColumn.SortPropertyName))
{
ListSortDirection direction;
// determine if this is a new sort, or a switch in sort direction.
if (lastSortedOnColumn == null
|| String.IsNullOrEmpty(lastSortedOnColumn.SortPropertyName)
|| !String.Equals(sortableGridViewColumn.SortPropertyName, lastSortedOnColumn.SortPropertyName, StringComparison.InvariantCultureIgnoreCase))
{
direction = ListSortDirection.Descending;
}
else
{
if (lastDirection == ListSortDirection.Ascending)
{
direction = ListSortDirection.Descending;
}
else
{
direction = ListSortDirection.Ascending;
}
}
// get the sort property name from the column's information.
string sortPropertyName = sortableGridViewColumn.SortPropertyName;
// Sort the data.
Sort(sortPropertyName, direction);
lastSortedOnColumn = sortableGridViewColumn;
lastDirection = direction;
}
}
}
}
public class SortList : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
//If the value is null tell binding engine to do nothing
if (value == null)
{
return Binding.DoNothing;
}
ListCollectionView view = (ListCollectionView)
CollectionViewSource.GetDefaultView(value);
SortDescription sort_isdisabled =
new SortDescription("#isenabled",
ListSortDirection.Ascending);
view.SortDescriptions.Add(sort_isdisabled);
SortDescription sort_id =
new SortDescription("#id",
ListSortDirection.Ascending);
view.SortDescriptions.Add(sort_id);
return view;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return Binding.DoNothing;
}
}
}
The XAML is as follows:
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:.....MiscellaneousCAESettings"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="0,0,0,0" >
<ControlTemplate x:Key="validationTemplate">
<DockPanel>
<TextBlock Foreground="Red" FontSize="20">!</TextBlock>
<AdornedElementPlaceholder/>
</DockPanel>
</ControlTemplate>
<Style x:Key="textBoxInError" TargetType="{x:Type TextBox}">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip"
Value="{Binding RelativeSource={x:Static RelativeSource.Self},
Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
<DataTemplate x:Key="textTemplate">
<TextBox HorizontalAlignment= "Stretch"
IsEnabled="{Binding XPath=./#isenabled}"
Validation.ErrorTemplate="{StaticResource validationTemplate}"
GotFocus="txtBoxGotFocus"
Style="{StaticResource textBoxInError}">
<TextBox.Text>
<Binding XPath="./#value" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<local:TextBoxMinMaxValidation>
<local:TextBoxMinMaxValidation.DataType>
<local:DataTypeCheck
Datatype="{Binding Source={StaticResource dataProvider}, XPath='/[#id=CustomerServiceQueueName]'}"/>
</local:TextBoxMinMaxValidation.DataType>
<local:TextBoxMinMaxValidation.ValidRange>
<local:Int32RangeChecker
Minimum="{Binding Source={StaticResource dataProvider}, XPath=./#min}"
Maximum="{Binding Source={StaticResource dataProvider}, XPath=./#max}"/>
</local:TextBoxMinMaxValidation.ValidRange>
</local:TextBoxMinMaxValidation>
</Binding.ValidationRules>
</Binding >
</TextBox.Text>
</TextBox>
</DataTemplate>
<DataTemplate x:Key="dropDownTemplate">
<ComboBox Name="cmbBox" HorizontalAlignment="Stretch"
SelectedIndex="{Binding XPath=./#value}"
ItemsSource="{Binding XPath=.//OPTION/#value}"
IsEnabled="{Binding XPath=./#isenabled}"
/>
</DataTemplate>
<DataTemplate x:Key="booldropDownTemplate">
<ComboBox Name="cmbBox" HorizontalAlignment="Stretch"
SelectedIndex="{Binding XPath=./#value, Converter={StaticResource boolconvert}}">
<ComboBoxItem>True</ComboBoxItem>
<ComboBoxItem>False</ComboBoxItem>
</ComboBox>
</DataTemplate>
<local:ControlTemplateSelector
x:Key="myControlTemplateSelector"/>
<Style x:Key="StretchedContainerStyle" TargetType="{x:Type ListViewItem}">
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="Template" Value="{DynamicResource ListBoxItemControlTemplate1}"/>
</Style>
<ControlTemplate x:Key="ListBoxItemControlTemplate1" TargetType="{x:Type ListBoxItem}">
<Border SnapsToDevicePixels="true" x:Name="Bd" Background="{TemplateBinding Background}" BorderBrush="{DynamicResource {x:Static SystemColors.ActiveBorderBrushKey}}" Padding="{TemplateBinding Padding}" BorderThickness="0,0.5,0,0.5">
<GridViewRowPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
</Border>
</ControlTemplate>
<Style x:Key="CustomHeaderStyle" TargetType="{x:Type GridViewColumnHeader}">
<Setter Property="Background" Value="LightGray" />
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="FontFamily" Value="Arial"/>
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="Padding" Value="2,0,2,0"/>
</Style>
</UserControl.Resources>
<Grid x:Name="GridViewControl" Height="Auto">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="34"/>
</Grid.RowDefinitions>
<ListView x:Name="ListViewControl" Grid.Row="0" ItemContainerStyle="{DynamicResource StretchedContainerStyle}"
ItemTemplateSelector="{DynamicResource myControlTemplateSelector}"
IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding Source={StaticResource dataProvider},
XPath=//CONFIGURATION}">
<ListView.View >
<GridView >
<GridViewColumn Header="ID" HeaderContainerStyle="{StaticResource CustomHeaderStyle}" DisplayMemberBinding="{Binding XPath=./#id}"/>
<GridViewColumn Header="VALUE" HeaderContainerStyle="{StaticResource CustomHeaderStyle}" CellTemplateSelector="{DynamicResource myControlTemplateSelector}" />
<GridViewColumn Header="DATATYPE" HeaderContainerStyle="{StaticResource CustomHeaderStyle}" DisplayMemberBinding="{Binding XPath=./#data_type}"/>
<GridViewColumn Header="DESCRIPTION" HeaderContainerStyle="{StaticResource CustomHeaderStyle}"
DisplayMemberBinding="{Binding XPath=./#description}"
Width="{Binding ElementName=ListViewControl, Path=ActualWidth}"/>
</GridView>
</ListView.View>
</ListView>
<StackPanel Grid.Row="1">
<Button Grid.Row="1" HorizontalAlignment="Stretch" Height="34" HorizontalContentAlignment="Stretch" >
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Center" Orientation="Horizontal" FlowDirection="RightToLeft" Height="30">
<Button Grid.Row="1" Content ="Apply" Padding="0,0,0,0 " Margin="6,2,0,2" Name="btn_Apply" HorizontalAlignment="Right" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" Width="132" IsTabStop="True" Click="btn_ApplyClick" Height="24" />
</StackPanel >
</Button>
</StackPanel >
</Grid>