I have listbox control which has list of colors. Here is code and Image:
<ListBox Name="FillSelections" VerticalContentAlignment="Stretch" HorizontalContentAlignment="Center" SelectedItem="{Binding SelectedColor}" SelectionMode="Single" Style="{StaticResource HorizontalListBoxStyle}" ItemsSource="{Binding FillColors}" ItemTemplate="{StaticResource ColorsItemTemplate}"></ListBox>
<DataTemplate x:Key="ColorsItemTemplate">
<Border BorderBrush="Transparent">
<Rectangle Width="20" StrokeThickness="1" Stroke="Black">
<Rectangle.Fill>
<SolidColorBrush Color="{Binding}" />
</Rectangle.Fill>
</Rectangle>
</Border>
Image:
How would I change style of last item only like this:
This can be achieved through converter which do the work of finding if its last item in the listbox -
Converter
public class IsLastItemInContainerConverter : IValueConverter
{
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
DependencyObject item = (DependencyObject)value;
ItemsControl ic = ItemsControl.ItemsControlFromItemContainer(item);
return ic.ItemContainerGenerator.IndexFromContainer(item)
== ic.Items.Count - 1;
}
public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
And using that you can set the DataTemplate in your xaml class like this -
<ListBox ItemContainerStyle="{StaticResource ColorsItemStyle}"/>
<Style x:Key="ColorsItemStyle">
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Self},
Converter={StaticResource IsLastItemInContainerConverter}}" Value="False">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate></DataTemplate> // Your template goes here
</Setter.Value>
</Setter>
</DataTrigger>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Self},
Converter={StaticResource IsLastItemInContainerConverter}}" Value="True">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate></DataTemplate> // Your lastItem template goes here
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
to get this to work w/ a ListBox that changes over time I ended up using a MultiBinding:
<DataTemplate x:Key="myItemTemplate">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding}"/>
<TextBlock x:Name="dots" Text="..."/>
</StackPanel>
<DataTemplate.Triggers>
<DataTrigger Value="False">
<DataTrigger.Binding>
<MultiBinding Converter="{StaticResource isLastItemInContainerConverter}">
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType=ListBoxItem}" />
<Binding Path="Items.Count" RelativeSource="{RelativeSource FindAncestor, AncestorType=ListBox}" />
</MultiBinding>
</DataTrigger.Binding>
<Setter TargetName="dots" Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
Note: the second binding is only used to get notified when the list changes
here is the corresponding MultivalueConverter
public class IsLastItemInContainerConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
DependencyObject item = (DependencyObject)values[0];
ItemsControl ic = ItemsControl.ItemsControlFromItemContainer(item);
return ic.ItemContainerGenerator.IndexFromContainer(item) == ic.Items.Count - 1;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
Stop Messing With The UI... It's Just Data!!!
Personally, I think the easiest way to do this is using a CompositeCollection (or a custom enumerator). The advantage of this way of thinking is it properly separates this as data, which is what it is, rather than messing with custom UI nonsense/bindings/relative sources, etc.
I'll explain.
Consider you are trying to show 'x' number of dynamically-generated colors stored in a myColors collection, followed by something that means 'no color' (your box with the line in it.)
First, define a 'no color' token somewhere in your app, like so...
class NoColorToken{}
Then define a DataTemplate targeting that class, like so...
<DataTemplate DataType="{x:Type ns:NoColorToken}">
<TextBlock Text="Replace with template representing 'no color'" />
</DataTemplate>
You could even make it more generic calling it a NoSelectionToken to use with any type of list. Just make sure to scope the DataTemplate to that specific location's usage (i.e. no color in this example.)
Then in your code, just stuff your colors into a CompositeCollection followed by an instance of the NoColorToken class, like so:
var colorsAndToken = new CompositeCollection();
colorsAndToken.Add(new CollectionContainer(myColors));
colorsAndToken.Add(new NoColorToken());
itemsControl.ItemsSource = colorsAndToken;
Changes to MyColors (if observable) will automatically update the UI.
Things can be made even easier if they don't need to be observable (i.e. No individual adds or removals) by simply writing an enumerator function (essentially the simplified basics of what CompositeCollection does internally.)
IEnumerable ColorsWithToken(IEnumerable colors){
foreach (var color in colors)
yield return color;
yield return new NoColorToken();
}
itemsControl.ItemsSource = ColorsWithToken(myColors);
Again though, the custom enumerator approach won't track changes to myColors. If myColors changes, you have to re-assign ItemsSource. However, if you go the route of the CompositeCollection, it handles updates automatically, just at the expense of a new object, the CompositeCollection, but that's what it's there for.
By the way, you can also wrap the above in a converter that handles either approach for you, returning either the enumerator, or a CompositeCollection for a pure XAML approach regardless of which ItemsControl.ItemsSource you're applying it to. I've actually done exactly that with an AddNoSelectionPlaceholder converter.)
Again, the reason I prefer this is it treats the items, including the 'no color' item as data, which is what it is. Even better, since it is data, it lets you easily change things around. Want the 'no color' item to be first? Just switch the order you added them.
colorsAndToken.Add(new NoColorToken());
colorsAndToken.Add(new CollectionContainer(myColors));
or
yield return new NoColorToken();
foreach (var color in colors)
yield return color;
Again, it's just data now. Nothing 'clever' needs to be done in the data template or control, bindings or anywhere else. Even better, it's now also fully unit-testable. No UI needed.
Related
I have a DataTemplate with a Path in a ResourceDictionary and I want to change the Fill color for the Path (would choose between two colors) depending on a bool property from the viewmodel.
<DataTemplate x:Key="FileIcon">
<Path Data="M20.8573547,8.0085467..." Fill="#F0F1F3" Width="30" Height="30"/>
</DataTemplate>
I presume, I need to use some converter but not sure how to write the XAML code for it. Something like this?
<Path Fill="{Binding MyBoolProperty, RelativeSource={RelativeSource FindAncestor, AncestorType=Path}, Converter={StaticResource BoolToColorConverter}}"/>
The Path isn't an ancestor of itself. If the viewmodel is the Path's DataContext, a conventional binding should suffice:
<Path
Fill="{Binding MyBoolProperty, Converter={StaticResource BoolToColorHiddenConverter}}"
/>
You could also skip the converter and use a Style trigger. Note that the default Fill is no longer set as an attribute in this version; if it is, then it'll override anything the Style does.
<Path
Data="M20.8573547,8.0085467..."
Width="30"
Height="30"
>
<Path.Style>
<Style TargetType="Path">
<Setter Property="Fill" Value="#F0F1F3" />
<Style.Triggers>
<DataTrigger
Binding="{Binding MyBoolProperty}"
Value="True"
>
<Setter Property="Fill" Value="FluorescentBeige" />
</DataTrigger>
</Style.Triggers>
</Style>
</Path.Style>
</Path>
If you want to use a converter, you can follow this example code for making one:
1. Make a new class
2. Use the following namespaces:
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
3. Inherit and implement the IValueConverter Interface
4. In the Convert function, evaluate the value parameter and return the corresponding color you want
Example Code
class BoolToColorConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if ((bool)value == true)
{
// return the color you want
}
else
{
// return the color you want
}
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
I am new to this all (this is my first post), so please bear with me. Any improvements is more then welcome.
I am writing an appointment scheduler using .Net 4.0, visual studio 2010. The XAML consists of a DataGrid with rows that are 15minutes apart and four columns, the left most column used for time. The backing data consists of AppointmentRows, an ObservableCollection. Each row itself consisting of an ObservalbeCollection of Appointmnets. I am using DragAndDrop as my input method.
DragAndDrop seems to work correctly to the cell-level. Items can be dropped on the datagrid, removed from the datagrid and rearranged within the datagrid. When an appointment is dropped on the datagrid, the first DataTrigger sets the custom attached propety of HasAppointment to true and the second Trigger responds to this attached property by setting the DataGridCell background color to one of four colors depending on whether the cell is now empty, contains an appointment of somebody who is new, who is old, or who requires special management.
Problem#1:
After loading the DataGrid, the Trigger responsible for coloring the background of a cell does not fire if that cell has been previously used—even though the DataTrigger responsible for setting the HasAppointment attached property does. That is, during loading and any time an UNUSED cell has an appointment dropped on it, all the triggers work correctly. However, once the triggers have fired on the cell—as when first being loaded or when a new appointment was dropped on it—the trigger responsible for background coloring never fires despite the first trigger correctly setting the HasAppointment attached property correctly and the cell returns to its first background coloring of the first appointment it contained. To be clear:
DataGrid is loaded. Cell has its default color of green background and is empty.
User drops a new appointment on the cell. All triggers fire and cell is colored yellow.
User then drags the contents of that cell to another place on the grid. Cell where the appointment WAS correctly reports as being empty, HasAppointment is false, and the color returns to the default green background.
User now drops another appointment on that cell. The DataTrigger responsible for setting the HasAppointment attached property fires correctly and sets the cell HasAppointment to true.
The Trigger for coloring does not fire and the cell returns to the yellow background of the first appointment it contained.
Problem#2:
After dropping an appointment on a cell, the DataTrigger resonsible for setting the HasAppointment associacted property runs twice before allowing the Trigger responsible for coloring to run. It runs correctly, but why twice?
Problem#3:
Each cell may or may not have an appointment. Each cell that has an appoinment maybe one of three collors. So what is the best way of returning multiple different values from a converter to a DataTrigger without repeating the code for each value? At this momemt I am using a Trigger, which works well WHEN IT WORKS.
Problem#4:
Can a DataTrigger operate on an attached property, and if it can, how is the binding set?
Thank you for any help to these matters.
Here is the XAML:
<Window x:Class="Chaos.Scheduler.Scheduler"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Chaos.Scheduler"
xmlns:my1="clr-namespace:UpDownCtrls;assembly=UpDownCtrls"
Title="Scheduler" Height="556" Width="1024"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
WindowStartupLocation="CenterScreen">
<!--Resources is a dictionary. so it allows assigning an x:Key value to Styles.-->
<Window.Resources>
<!--Declare converters for custom binding logic-->
<local:RescheduleConverter x:Key="rescheduleConverter" />
<local:ColorConverter x:Key="colorConverter" />
<local:AppointmentConverter x:Key="appointmentConverter" />
<local:WatchApppointmentNamesConverter x:Key="watchAppointmentNamesConverter" />
<!--Show buttons as Red when an edit has occured-->
<Style x:Key="SaveButtonStyle" TargetType="{x:Type Button}">
<Style.Triggers>
<DataTrigger Binding="{Binding HasEdits}" Value="True" >
<Setter Property="Background" Value="Red" />
</DataTrigger>
</Style.Triggers>
</Style>
<!--The XAML will create a "Style" object/instance of type "DataGridCell". Without an x:Key value, this "style" object
will be used as the parent style for all the DataGridCells. -->
<Style TargetType="{x:Type DataGridCell}" x:Key="AppointmentStyle" >
<EventSetter Event="PreviewMouseLeftButtonDown" Handler="DataGridCell_PreviewMouseLeftButtonDown" />
<EventSetter Event="MouseMove" Handler="DataGridCell_MouseMove" />
<EventSetter Event="DragEnter" Handler="DataGridCell_DragEnter" />
<EventSetter Event="Drop" Handler="DataGridCell_Drop" />
<!--Must use Right/LeftButtonUp and not Right/LeftButtonDown as these events are Direct, not tunneling or bubbled.-->
<!--This makes no sense and is probably a bug, but it works-->
<EventSetter Event="PreviewMouseRightButtonUp" Handler="DataGridCell_PreviewMouseRightButtonUp" />
<!--There is no way to replace only part of the visual tree of a control. To change the visual tree of a control you must set the
Template property of the control to its new and COMPLETE ControlTemplate. -->
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type DataGridCell}">
<!--Define the DataGridCell content. Bind the Grid control background color to the DataGridCell Style template.
The Background color will be inherited by the textblock (that is within the <Grid> by default).-->
<Grid Background="{TemplateBinding Background}">
<ContentPresenter AllowDrop="True" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
<!--Set default background color for the entire row-->
<Setter Property="Background" Value="Green"/>
<Style.Triggers>
<Trigger Property="local:Scheduler.HasAppointment" Value="true">
<Setter Property="Background">
<Setter.Value>
<MultiBinding Converter="{StaticResource colorConverter}" >
<MultiBinding.Bindings>
<!--Sends the "DataGirdCell" object to the converter-->
<Binding RelativeSource="{RelativeSource Self}"></Binding>
<!--Sends the current "DataGridRow" to the converter. The DataGridRow is the DataContext for the cell -->
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type DataGridRow}, AncestorLevel=1}" Path="." />
</MultiBinding.Bindings>
</MultiBinding>
</Setter.Value>
</Setter>
</Trigger>
<!--If the Converter returns True, then the Background will be red for the row-->
<DataTrigger Value="True">
<DataTrigger.Binding>
<!--The DataTrigger is operating on the entire row-->
<MultiBinding Converter="{StaticResource watchAppointmentNamesConverter}" ConverterParameter="0">
<!--send the DataGridCell in effect when the displayname properites change-->
<Binding RelativeSource="{RelativeSource Self}"></Binding>
<!--binding will watch the displaynames for each column. The AppointmentRow is the DataContext for all cells-->
<Binding Path="[0].displayname"></Binding>
<Binding Path="[1].displayname"></Binding>
<Binding Path="[2].displayname"></Binding>
</MultiBinding>
</DataTrigger.Binding>
<!--Any setter here will be applied to the entire row, not just the cell. There MUST HAVE be setter for the Converter to be executed-->
<Setter Property="Background" Value="Red"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid Name="MainGrid">
<!--Set AllowDrop="False" to show the scrollbars as not droppable targets-->
<DataGrid AutoGenerateColumns="False" CellStyle="{StaticResource AppointmentStyle}" ItemsSource="{Binding MySchedule.AppointmentRows}"
HorizontalAlignment="Stretch" Margin="302,47,0,37"
Name="dataGridScheduler" VerticalAlignment="Stretch" Width="688"
AllowDrop="False" SelectionUnit="Cell" SelectionMode="Single"
CanUserReorderColumns="False" CanUserAddRows="False" IsReadOnly="True" >
<!--Must have AllowDrop = True to allow dropping on the cells-->
<DataGrid.RowStyle>
<Style TargetType="{x:Type DataGridRow}">
<Setter Property="AllowDrop" Value="True" />
</Style>
</DataGrid.RowStyle>
<DataGrid.Columns>
<!--{Binding /, Path=[0].displayname} is binding to the current item of the collection, the appointment row,
with index of 0, the appointment, and shows the property of appointment displayname. The displayname must use the INotifyPropertyChanged interface-->
<DataGridTextColumn Binding="{Binding time}" Header="Time" Width="58" IsReadOnly="True" />
<DataGridTextColumn Binding="{Binding /, Path=[0].displayname}" Header="Name" Width="240*" />
<DataGridTextColumn Binding="{Binding /, Path=[1].displayname}" Header="Name" Width="240*" />
<DataGridTextColumn Binding="{Binding /, Path=[2].displayname}" Header="Name" Width="240*" />
</DataGrid.Columns>
</DataGrid>
…..........
</Window>
Here is the code-behind:
#region Attached Property HasAppointment
// Attached properties are added to the control, not the data being displayed, and used by the XAML directly.
// Example useage in XAML: <Trigger Property="local:Scheduler.HasAppointment" Value="True">
// The attached property template is obtained by typing <propa> tab tab into the code-behind.
public static Boolean GetHasAppointment(DependencyObject obj)
{
return (Boolean)obj.GetValue(HasAppointmentProperty);
}
public static void SetHasAppointment(DependencyObject obj, Boolean value)
{
obj.SetValue(HasAppointmentProperty, value);
}
// Using a DependencyProperty as the backing store for HasAppointment. This enables animation, styling, binding, etc...
public static readonly DependencyProperty HasAppointmentProperty =
DependencyProperty.RegisterAttached("HasAppointment", typeof(Boolean), typeof(Scheduler), new UIPropertyMetadata(false));
#endregion
/* Converters apply custom logic to the XAML Bindings */
/// <summary>
/// Tests the DataGridCell for a displayname. Returns false for no displayname, true for a display name.
/// </summary>
[ValueConversion(typeof(String), typeof(Boolean))]
public class AppointmentConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
String displayname = value as String;
if (string.IsNullOrEmpty(displayname))
return false;
return true;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
/// <summary>
/// Label the reschedule shape with "Reschedule Appointment" or the patient name.
/// </summary>
[ValueConversion(typeof(Appointment), typeof(String))]
public class RescheduleConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
Appointment apt = value as Appointment;
if (apt.displayname == null) return "Reschedule Appointment";
else return apt.displayname;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
/* Color a grid cell based on appointment type and patient history */
public class ColorConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (values[1] is DataGridRow)
{
// The "cell" tells nothing about its contents, only its location in the grid, its column name and column position.
// The datacontext comes from the datagrid row.
DataGridCell cell = (DataGridCell)values[0];
if (cell.Column.DisplayIndex > 0)
{
Boolean hasappointment = Scheduler.GetHasAppointment(cell);
// the object "row" is of type AppointmentRow.
DataGridRow row = (DataGridRow)values[1];
// gets the physical row number of this row in the grid.
int rowIndex = row.GetIndex();
// SortMemberPath returns: "time", "[0].displayname", " "[1].displayname", "[2].displayname"
string columnName = cell.Column.SortMemberPath;
Appointment appointment = ((AppointmentRow)row.Item)[cell.Column.DisplayIndex - 1] as Appointment;
if (null != appointment.displayname)
{
if ((appointment.type & AppointmentType.Excision) == AppointmentType.Excision)
{
// return new SolidColorBrush(Colors.Red);
return Brushes.Red;
}
else if (null == appointment.lastvisit)
{
// return new SolidColorBrush(Colors.Yellow);
return Brushes.Yellow;
}
}
}
}
// return System.Windows.SystemColors.AppWorkspaceColor;
return new SolidColorBrush(Colors.White);
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
/// <summary>
/// Set the HasAppointment flag for all cells in the AppointmentRow.
/// For some reason, this routine will process the entire row twice on a drop. I suspect setting the HasAppointment flag forces the
/// second processeing as the "cell" object itself maybe changed.
/// [0] = DataGridCell
/// [1] = AppointmentRow[0].displayname
/// [2] = AppointmentRow[1].displayname
/// [3] = AppointmentRow[2].displayname
/// </summary>
public class WatchApppointmentNamesConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
DataGridCell cell = values[0] as DataGridCell;
if (!String.IsNullOrWhiteSpace((string)values[1]))
{
if (cell.Column.DisplayIndex == 1)
Scheduler.SetHasAppointment(cell, true);
}
else
{
if (cell.Column.DisplayIndex == 1)
Scheduler.SetHasAppointment(cell, false);
}
if (!String.IsNullOrWhiteSpace((string)values[2]))
{
if (cell.Column.DisplayIndex == 2)
Scheduler.SetHasAppointment(cell, true);
}
else
{
if (cell.Column.DisplayIndex == 2)
Scheduler.SetHasAppointment(cell, false);
}
if (!String.IsNullOrWhiteSpace((string)values[3]))
{
if (cell.Column.DisplayIndex == 3)
Scheduler.SetHasAppointment(cell, true);
}
else
{
if (cell.Column.DisplayIndex == 3)
Scheduler.SetHasAppointment(cell, false);
}
// need to return false or will force background to red.
return false;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
I still can't figure out how to use (or if they work) triggers on attached properties. I would sure like to see how. In the meantime, I found this solves most of my problems fairly easily:
<DataGridTextColumn.ElementStyle>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Background" >
<Setter.Value>
<MultiBinding Converter="{StaticResource colorTextConverter}">
<Binding RelativeSource="{RelativeSource Mode=Self}" Path="Text" />
<Binding RelativeSource="{RelativeSource Mode=FindAncestor,AncestorType=DataGridCell}" Path="." />
</MultiBinding>
</Setter.Value>
</Setter>
</Style>
</DataGridTextColumn.ElementStyle>
</DataGridTextColumn>
Simple binding in this case does correctly watch for changes to the display--which the triggers would miss.
I need to do something similar to a PriorityBinding, except the lower priority binding is used when the binding is null (not when the binding is invalid like in a PriorityBinding). I can't seem to figure out a 'good' way to do this short of creating two duplicate controls one with each binding I want to use and triggering their visibility based on whether or not the binding is null. There must be a better way as I do not want to update two controls every time I need to change something (duplicate code == bad).
Example:
when SelectedItem.SelectedItem is not null:
<ContentControl Content="{Binding SelectedItem.SelectedItem}"/>
and when SelectedItem.SelectedItem is null:
<ContentControl Content="{Binding SelectedItem}"/>
using a Style like this did not work:
<ContentControl Content="{Binding SelectedItem.SelectedItem}">
<ContentControl.Style>
<Style TargetType="ContentControl">
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedItem.SelectedItem}" Value="{x:Null}">
<Setter Property="Content" Value="{Binding SelectedItem}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
I'm guessing this did not work because the bindings in the style are trying to use the ContentControl's Content property as their source so the DataTrigger is acually testing SelectedItem.SelectedItem.SelectedItem.SelectedItem. Any ideas?
You could use MultiBinding to achive what you want:
<ContentControl Content="{Binding SelectedItem.SelectedItem}">
<ContentControl.Content>
<MultiBinding Converter="{StaticResource ResourceKey=myConverter}">
<Binding Path="SelectedItem"/>
<Binding Path="SelectedItem.SelectedItem"/>
</MultiBinding>
</ContentControl.Content>
</ContentControl>
Your Converter could look like
public class MyMultiConverter:IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values[1] == null)
return values[0];
return values[1];
}
public object[] ConvertBack(object values, Type[] targetType, object parameter, CultureInfo culture)
{
return null;
}
}
I have a DataGrid with many columns and I'd like to provide the users with a drop down that allows them to select which columns they can see. I'm using the .Net 4 WPF DataGrid in a desktop application.
Does anyone know of an easy way to accomplish what I am trying to do.
I do this as follows.
I derive from the grid an add an ICommand called HideShowColumnCommand that takes as its parameter a DataGridColumn (the one I want to hide or show) and hides the column if it is visible, and shows it if it is not.
Then I use a tricky context menu that I attach to the column header that has a tick that shows the column visible/hidden state..
The context menu looks like so
<ContextMenu
ItemsSource="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Column.DataGridOwner.Columns}">
<ContextMenu.Resources>
<local:DataGridHeaderVisibilityToBooleanConverter
x:Key="visibilityConverter" />
<BooleanToVisibilityConverter
x:Key="VisibilityOfBool" />
<DataTemplate
DataType="{x:Type DataGridColumn}">
<ContentPresenter
Content="{Binding Path=Header}"
RecognizesAccessKey="True" />
</DataTemplate>
</ContextMenu.Resources>
<ContextMenu.ItemContainerStyle>
<Style
TargetType="MenuItem">
<!--Warning dont change the order of the following two setters
otherwise the command parameter gets set after the command fires,
not much use eh?-->
<Setter
Property="CommandParameter"
Value="{Binding Path=.}" />
<Setter
Property="Command"
Value="{Binding Path=DataGridOwner.HideShowColumnCommand}" />
<Setter
Property="IsChecked"
Value="{Binding Path=Visibility, Converter={StaticResource visibilityConverter}}" />
</Style>
</ContextMenu.ItemContainerStyle>
</ContextMenu>
The converter like this
public class DataGridHeaderVisibilityToBooleanConverter :IValueConverter{
#region IValueConverter Members
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
try {
Visibility visibility = (Visibility)value;
if (visibility == Visibility.Visible) {
return true;
}
else {
return false;
}
}
catch { }
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
throw new NotImplementedException();
}
#endregion
}
I have a textbox and a checkbox, i would like to set three properties on the textbox based on whether the checkbox is checked or not.
I can bind the properties to the checkbox but then i need to know what property is being bound in the converter.
For example, when unchecked i would like the textbox properties to be AcceptsReturn="False" TextWrapping="NoWrap" Height="25".
Then checked: AcceptsReturn="True" TextWrapping="Wrap" Height="100".
Will this require 3 converters or can i tell the converter "if checked==true && boundfrom == height, return 100"
Thanks,
Kohan
Accepted Solution
<TextBox Name="txtAnswer" Margin="5" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Hidden" >
<TextBox.Style>
<Style>
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=cbMultiLine, Path=IsChecked}" Value="True">
<Setter Property="TextBox.TextWrapping" Value="Wrap" />
<Setter Property="TextBox.Height" Value="100" />
<Setter Property="TextBox.AcceptsReturn" Value="True" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBox.Style>
</TextBox>
This should work with one converter by using the ConverterParameter property on the binding:
Converter="{StaticResource MyIsCheckedConverter}" ConverterParameter="height"
The Converter would look like:
public class IsCheckedConverter : IValueConverter
{
public object Convert(
object value, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
{
object returnValue;
if (!((bool?)value).HasValue)
//do something if null (but I don't see why it would be)
switch ((string) parameter)
{
case "height":
returnValue = ((bool?)value).Value ? 100 : 25;
break;
case "TextWrapping":
.....
.....
}
return returnValue;
}
public object ConvertBack(
object value, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
{
//Likewise for convert back but I think in your case you wouldn't need to convert back
}
}
i would seriously consider using a data trigger. its a ui concern so i would try to avoid using your view model. you could do this with just a few lines of xaml.
<CheckBox x:Name="myCheckBox" />
<TextBox
AcceptsReturn="{Binding ElementName=myCheckBox, Path=IsChecked}"
TextWrapping="{Binding ElementName=myCheckBox, Path=IsChecked, Converter={StaticResource boolToTextWrappingConverter}}"
Height="{Binding ElementName=myCheckBox, Path=IsChecked, Converter={StaticResource boolToHeightConverter}}"
/>
this would reduce it to 2 converters. you could also write one boolToTextWrappingOrHeight converter as you suggested in your post, and pass in a CommandParameter=height and CommandParameter=textwrapping and look at the parameter in the converter, but I'm not a fan of that approach. A third option would be to create IsChecked, TextWrapping and Height properties in your viewmodel, bind to those, and put your conversion logic in the properties.