I was trying to solve the following problem (and finally succeeded but probably not in the best way). This is how I tried first:
I am showing a treeview with directories and a checkbox with this WPF code:
<Window.DataContext>
<local:ViewModel/>
</Window.DataContext>
<Grid>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<StackPanel.Resources>
<!-- This Style is applied to all TextBlock elements in the command strip area. -->
<Style TargetType="TextBlock">
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Foreground" Value="#EE000000" />
</Style>
<local:ColorConverter x:Key="XcolorConverter" />
</StackPanel.Resources>
<TreeView ItemsSource="{Binding View}">
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type local:Folder}" ItemsSource="{Binding SubFolders}">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Background="{Binding Path=., Converter={StaticResource XcolorConverter}}" Text="{Binding Name}"/>
<CheckBox Focusable="False" IsChecked="{Binding IsChecked}" VerticalAlignment="Center"/>
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.Resources>
</TreeView>
</StackPanel>
</Grid>
What I would need to know in the ColorConverter method Convert, below, is the full directory name to color directories which meet a specific criterium. Parameter "value" is a string with the value (MyNameSpace).Folder. If I inspect "value" in the debugger, I also see "Name" which is the directory name (without the preceding full path) displayed in the Treeview's textbox. However, I can not access value:Name within the program (error CS1061: 'object' does not contain a definition for 'Name', I don't understand why I can see it in the debugger but not access it) nor would it help me as I need the full directory path. Within the ViewModel class/code there's a ForEach assigning the directory names to the ObservableCollection Folder. The object parameter is empty; I know I could add ConverterParameter= in the xaml but don't know how to access the actual displayed directory from within that xaml.
How should I change the WPF so my colorConverter.Convert method can access the (full) directory it is displaying at that moment?
public ICollectionView View { get => cvs.View; }
private CollectionViewSource cvs = new CollectionViewSource();
private ObservableCollection<Folder> col = new ObservableCollection<Folder>();
public class Folder
{
public string Name { get; set; }
public ObservableCollection SubFolders { get; set; } = new ObservableCollection();
}
public partial class ColorConverter : IValueConverter
{
private static int count;
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{ // Set color based upon directory, something like if paramater.(directory=c:\\temp")...
return Brushes.Green;
}
}
If I understood correctly what you need:
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
// Validating the type of incoming value
if (value is Folder folder)
{
// Here we work with the folder variable
string name = folder.Name;
// Set color based upon directory, something like if paramater.(directory=c:\\temp")...
return Brushes.Green;
}
// If the received value is not of the Folder type,
// then the converter returns an undefined value.
return DependencyProperty.UnsetValue;
}
Related
I have am attempting to build a tree view where:
1. The TreeViewItems are generated by a list in my model.
2. Each TreeViewItem contains a ComboBox, and a dynamic element whose template I want to change based on the value selected in the ComboBox.
Here is my current xaml code.
<Window x:Class="MyTestWPF.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:MyTestWPF"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<local:NodeTypeToTemplateConverter x:Key="NodeTypeToTemplateConverter"/>
<DataTemplate x:Key="Template1">
<TextBlock Text="Template 1" />
</DataTemplate>
<DataTemplate x:Key="Template2">
<TextBlock Text="Template 2" />
</DataTemplate>
<Style x:Key="MyNodeTemplate" TargetType="ContentPresenter">
<Setter Property="ContentTemplate" Value="{StaticResource Template1}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Path=NodeType}">
<DataTrigger.Value>
<local:NodeTypesEnum>Type1</local:NodeTypesEnum>
</DataTrigger.Value>
<Setter Property="ContentTemplate" Value="{Binding Converter={StaticResource NodeTypeToTemplateConverter}}"/>
</DataTrigger>
</Style.Triggers>
</Style>
<HierarchicalDataTemplate DataType="{x:Type local:MyTreeNode}"
ItemsSource="{Binding Nodes}">
<StackPanel Orientation="Horizontal">
<ComboBox ItemsSource="{Binding Path=GetAvailableNodeType}"
SelectedItem="{Binding Path=NodeType}" />
<ContentPresenter Style="{StaticResource MyNodeTemplate}" Content="{Binding}" />
</StackPanel>
</HierarchicalDataTemplate>
</Window.Resources>
<TreeView x:Name="MyTree" ItemsSource="{Binding MyTreeModel}" />
</Window>
And its code-behind:
using System.Windows;
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new
{
MyTreeModel = new MyTreeNode[] {
new MyTreeNode() { Name = "1", Nodes = new MyTreeNode[] { new MyTreeNode() { Name= "2" } } }
}
};
}
}
The tree node type:
namespace MyTestWPF
{
public class MyTreeNode
{
public string Name { get; set; }
public NodeTypesEnum NodeType { get; set; }
public MyTreeNode[] Nodes { get; set; }
public NodeTypesEnum[] GetAvailableNodeType()
{
return new NodeTypesEnum[] { NodeTypesEnum.Type1, NodeTypesEnum.Type2 };
}
}
public enum NodeTypesEnum
{
Type1 = 0,
Type2 = 1
}
}
The Converter (NodeTypeToTemplateConverter) receives the whole ViewModel, and returns the name of the relevant template based on values in the model.
using System;
using System.Globalization;
using System.Windows.Data;
namespace MyTestWPF
{
public class NodeTypeToTemplateConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if((value as MyTreeNode).NodeType == NodeTypesEnum.Type1)
{
return "Template1";
} else
{
return "Template2";
}
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
The problem is that the above code causes a stack overflow exception. The first item in the TreeView endlessly calls NodeTypeToTemplateConverter's Convert method.
I figured it had to do with the DataTrigger.Value. Setting that to a value different from the default NodeType allows the page to load without overflow, but the moment any ComboBox is set to NodeType1, stack overflow.
I attempted to simply remove the DataTrigger.Value element, but that causes the Converter to never be called at all...
How can I dynamically build the template name based on the value selected by its neighboring ComboBox?
You probably want to use a DataTemplateSelector rather than a converter.
public class ComboBoxItemTemplateSelector : DataTemplateSelector
{
public DataTemplate Template1 { get; set; }
public DataTemplate Template2 { get; set; }
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
//Logic to select template based on 'item' value.
if (item == <template1Value>) return Template1; //TODO: replace <template1Value>
else if (item == <template2Value>) return Template2; //TODO: replace <template2Value>
else return new DataTemplate();
}
}
<local:ComboBoxItemTemplateSelector x:Key="ComboBoxItemTemplateSelector">
<local:ComboBoxItemTemplateSelector.Template1>
<DataTemplate>
<TextBlock Text="" />
</DataTemplate>
</local:ComboBoxItemTemplateSelector.Template1>
<local:ComboBoxItemTemplateSelector.Template2>
<DataTemplate>
<TextBlock Text="" />
</DataTemplate>
</local:ComboBoxItemTemplateSelector.Template2>
</local:ComboBoxItemTemplateSelector>
<ContentPresenter Content="{Binding NodeType}" ContentTemplateSelector="{StaticResource ComboBoxItemTemplateSelector}"/>
I have not fully tested this code, so let me know if you have any issues.
EDIT:
The template selector is only executed when the content changes so this won't work if you use {Binding}. A workaround for this would be to have the DataTemplate content bind to the parent's DataContext.
<DataTemplate>
<TextBlock Text="" DataContext="{Binding DataContext, RelativeSource={RelativeSource AncestorType=ContentPresenter}}"/>
</DataTemplate>
If this workaround is not acceptable, there are other ways to do this as well.
In my application I would like you highlight an index based on the value. for example:
ArrayList itemsList = new ArrayList();
private void button_Click(object sender, RoutedEventArgs e)
{
itemsList.Add("Coffie");
itemsList.Add("Tea");
itemsList.Add("Orange Juice");
itemsList.Add("Milk");
itemsList.Add("Mango Shake");
itemsList.Add("Iced Tea");
itemsList.Add("Soda");
itemsList.Add("Water");
listBox.ItemsSource = itemsList;
ApplyDataBinding();
}
private void ApplyDataBinding()
{
listBox.ItemsSource = null;
listBox.ItemsSource = itemsList;
}
It does not matter where in the listbox "Orange Juice" is I would like to highlight it based on its value. If the Position change it should be still highlighted. (Not based on the selected index)
If you want to highlight an item based on it's value, then you need to define your own datatemplate for an item and use a converter to provide appropriate brush for the background. Something like that:
<Window.Resources>
<local:TextToBrushConverter x:Key="TextToBrushConverter" />
</Window.Resources>
<Grid>
<ListBox Name="listBox" HorizontalContentAlignment="Stretch">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" Background="{Binding ., Converter={StaticResource TextToBrushConverter}}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
Converter
class TextToBrushConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if ((value as String) == "Orange Juice")
{
return Brushes.Orange;
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
I would recommend that you place the items into a class, think Object Oriented design and then work off of properties (as flags) to give different states.
Then by using Xaml styles to key off those different properties to achieve the affect you are looking for.
Say for example we have an Order class with these properties
public class Order
{
public string CustomerName { get; set; }
public int OrderId { get; set; }
public bool InProgress { get; set; }
}
When an order is marked as in progress (InProgress = true) we want to show red in our list box say for "Alpha" and "Omega" which are in progress:
ListBox Xaml
Here is the Xaml which binds to our data (how you bind is up to you) and shows how to work with Style(s), DataTemplate, and DataTrigger(s) to achieve that:
<ListBox ItemsSource="{StaticResource Orders}"
x:Name="lbOrders">
<ListBox.Resources>
<DataTemplate DataType="{x:Type model:Order}">
<TextBlock Text="{Binding Path=CustomerName}" />
</DataTemplate>
<Style TargetType="{x:Type ListBoxItem}">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=InProgress}"
Value="True">
<Setter Property="Foreground"
Value="Red" />
</DataTrigger>
</Style.Triggers>
</Style>
</ListBox.Resources>
</ListBox>
Here is the data setup in the pages's resource Xaml to do that, but it could be created in code behind:
<Window.Resources>
<model:Orders x:Key="Orders">
<model:Order CustomerName="Alpha"
OrderId="997"
InProgress="True" />
<model:Order CustomerName="Beta"
OrderId="998"
InProgress="False" />
<model:Order CustomerName="Omega"
OrderId="999"
InProgress="True" />
<model:Order CustomerName="Zeta"
OrderId="1000"
InProgress="False" />
</model:Orders>
</Window.Resources>
This should give you enough to start on and create a full featured UI.
I have an wpf Expander with templated header. In this template, i have an TextBox, which use Binding with Converter to set MaxLines and MinLines, which depends on Expander.IsExpanded.
The idea is to let user see first line of text and show more when Expander is expanded (alternate solution would be to make that TextBox.Visiblitity = Collapsed when expanded and another TextBox.Visibility = Visible, but user will lost their cursor position, marked text and i dont know what else)
<UserControl>
<UserControl.Resources>
<DataTemplate x:Key="MyTemplate">
<Grid>
<Grid.Resources>
<Converters:ExpandedToLineRowsConverter ExpandedLines="5"
CollapsedLines="1"
x:Key="ExpandedToLines"/>
</Grid.Resources>
<TextBox MaxLines="{Binding IsExpanded,
Converter={StaticResource ExpandedToLines},
RelativeSource={RelativeSource FindAncestor,
AncestorLevel=1,
AncestorType={x:Type Expander}},
UpdateSourceTrigger=PropertyChanged}"
MinLines="{Binding IsExpanded,
Converter={StaticResource ExpandedToLines},
RelativeSource={RelativeSource FindAncestor,
AncestorLevel=1,
AncestorType={x:Type Expander}},
UpdateSourceTrigger=PropertyChanged}" />
</Grid>
</DataTemplate>
</UserControl.Resources>
<Expander Header="{Binding}"
HeaderTemplate="{StaticResource MyTemplate}">
<!-- other wpf controls under expander, they do not affect the problem -->
</Expander>
</UserControl>
ExpandedToLineRowsConverter is very simple:
public class ExpandedToLineRowsConverter : IValueConverter
{
public int ExpandedLines { get; set; }
public int CollapsedLines { get; set; }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return (bool)value ? ExpandedLines : CollapsedLines;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
return (int)value != CollapsedLines;
}
}
Problem is, it is working only when expanding, not when collapsing (textbox stay in multiline mode even when Expander.IsExpanded = false).
When i set breakpoint in converter, it is returning corect number of lines, but it looks like the TextBox just ignore them.
I have no idea what to do...
Edit: sample VS2012 project with problem
My Binding :
<StackPanel x:Name="Ancestor">
<StackPanel.Resources>
<converters:DiceInputToVisualConverter x:Key="MyDiceInputToVisualConverter" />
<Style TargetType="{x:Type Ellipse}">
<Setter Property="Visibility" Value="{Binding Path=/, Converter={StaticResource MyDiceInputToVisualConverter},FallbackValue=Visible}"></Setter>
</Style>
<StackPanel.Resources>
<StackPanel>
<Canvas DataContext="{Binding Path=DataContext.Dice1,RelativeSource={RelativeSource AncestorType=StackPanel}}">
<Ellipse Canvas.Left="5" Canvas.Top="5"></Ellipse>
<Ellipse Canvas.Left="5" Canvas.Top="20"></Ellipse>
</Canvas>
</StackPanel>
the DataContext :
Ancestor.DataContext = game ;
the converter :
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
int dice = int.Parse(value.ToString());
if (dice == 3)
return Visibility.Visible;
return Visibility.Hidden;
}
my data source :
public Class Game : INotifyPropertyChanged
{
private int dice1;
public int Dice1
{
get { return dice1; }
set
{
dice1 = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("Dice1"));
}
}
}
the binding is fine when i checked it with snoop the ellipse's DataContext had the desired value
but still the Converter is never called any ideas ?
Are you sure that you should be using Path=/? This notation means the currently selected item of the default collection view.
I see you have called a static resource
StaticResource MyDiceInputToVisualConverter
Where have you declared this resource. for example in the Window.Resources section Like the following.
<Window.Resources>
<!-- Converters that are used on the MainWindow. -->
<Converters:MyDiceInputToVisualConverter x:Key="MyDiceInputToVisualConverter" />
</Window.Resources>
I'm trying to get around the fact that I can't specify a dynamic value for ConverterParameter. See my other question for why I need to bind a dynamic value to ConverterParameter - I don't like the solutions currently posted because they all require what I feel should be unnecessary changes to my View Model.
To attempt to solve this, I have created a custom converter, and exposed a dependency property on that converter:
public class InstanceToBooleanConverter : DependencyObject, IValueConverter
{
public object Value
{
get { return (object)GetValue(ValueProperty); }
set { SetValue(ValueProperty, value); }
}
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register("Value", typeof(object), typeof(InstanceToBooleanConverter), null);
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return value != null && value.Equals(Value);
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return value.Equals(true) ? Value : Binding.DoNothing;
}
}
Is there a way to set this value using a binding (or style setter, or other crazy method) in my XAML?
<ItemsControl ItemsSource="{Binding Properties}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type local:SomeClass}">
<DataTemplate.Resources>
<!-- I'd like to set Value to the item from ItemsSource -->
<local:InstanceToBooleanConverter x:Key="converter" Value="{Binding Path=???}" />
</DataTemplate.Resources>
<!- ... ->
The examples I've seen so far only bind to static resources.
Edit:
I got some feedback that there is only one converter instance with the XAML I posted.
I can work around this by placing the resource in my control:
<ItemsControl ItemsSource="{Binding Properties}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type local:SomeClass}">
<RadioButton Content="{Binding Name}" GroupName="Properties">
<RadioButton.Resources>
<!-- I'd like to set Value to the item from ItemsSource -->
<local:InstanceToBooleanConverter x:Key="converter"
Value="{Binding Path=???}" />
</RadioButton.Resources>
<RadioButton.IsChecked>
<Binding Path="DataContext.SelectedItem"
RelativeSource="{RelativeSource AncestorType={x:Type Window}}"
Converter="{StaticResource converter}" />
</RadioButton.IsChecked>
</RadioButton>
<!- ... ->
So this problem isn't blocked by having to share an instance of the converter :)
Unfortunately this isn't going to work - I've been down this road before and it turns out all the Items in the ItemsControl share the same Converter. I think this is due to the way the XAML parser works.
Firstly you can specify the converter at a higher level resource dictionary and set x:Shared to false, secondly if you want to "set the Value to the item from ItemsSource" as you annotated you can just specify an empty binding (Value="{Binding}").