Listview with optional style - wpf

I have an application that has the option to load a custom theme. In that theme, I have a style for ListViewItem that changes the highlight color. In the application I have a GridView that has rows that can be double clicked, and looks like:
<UserControl.Resources>
<Style x:Key="ClickableRowStyle" TargetType="{x:Type ListViewItem}">
<EventSetter Event="MouseDoubleClick" Handler="RowDoubleClicked" />
</Style>
</UserControl.Resources>
...
<ListView ItemsSource="{Binding DataItems}" ItemContainerStyle="{StaticResource ClickableRowStyle}">
... Set up GridRows
</ListView>
The problem I am having is that since the grid above uses its own style, the colors from the theme don't get applied.
I have tried adding BasedOn="{StaticResource {x:Type ListViewItem}}". This works if the theme is loaded, however, if the default Windows style is used, I get an exception from the StaticResourceHolder.
Is there a way to get both the look from the theme (if loaded) while still being able to double click the grid rows?

I figured out how to have an optional style and click events at the same time. I set up a loaded event for the list view, and in the loaded event I create a new style using BasedOn if a ListViewItem style already exists.
Xaml:
<ListView x:Name="listView" ItemsSource="{Binding DataItems}" Loaded="ListView_Loaded">
... Set grid rows
</ListView>
C#:
private void ListView_Loaded(object sender RoutedEventArgs e)
{
ListView listView = sender as ListView;
Style style;
if(Application.Current.Resources.Contains(typeof(ListViewItem))
{
style = new Style(typeof(ListViewItem), (Style)Application.Current.Resources[typeof(ListViewItem)]);
}
else
{
style = new Style(typeof(ListViewItem));
}
EventSetter setter = new EventSetter();
setter.Event = ListViewItem.MouseDoubleClickEvent;
setter.Handler = new MouseButtonEventHandler(ListView_MouseDoubleClick);
style.Setters.Add(setter);
listView.ItemContainerStyle = style;
}

Related

WPF: How to hide specific tab item in TabControl that with DataTemplate bind to views

I have a WPF xaml TabControl defined as below, how to set Visibility to Collapsed for the tab item based on binding?
Current code:
<TabControl>
<DataTemplate DataType="{x:Type foo:FooViewModel}">
<foo:FooView/>
</DataTemplate>
<DataTemplate DataType="{x:Type bar:BarViewModel}">
<bar:BarView/>
</DataTemplate>
<TabControl/>
What I want to implement to only hide foo TabItem based on condition, but seems it cannot specify the target name?? E.g. hide the template which name is Foo. Thanks for your help, have a nice day.
<TabControl ItemsSource="{Binding ParentModel}">
<DataTemplate x:Name="Foo" DataType="{x:Type foo:FooViewModel}">
<foo:FooView/>
</DataTemplate>
<DataTemplate x:Name="Bar" DataType="{x:Type bar:BarViewModel}">
<bar:BarView/>
</DataTemplate>
<TabControl.ItemContinerStyle>
<Style TargetType="TabItem" 'e.g. TargetName="Foo"<==========='>
<Style.Triggers>
...Some trigger condition ignored here
<Setter Property="Visibility" Value="Collapsed" />
</Style.Triggers>
</Style>
<TabControl.ItemContinerStyle/>
<TabControl/>
Hiding the TabItem element is visually equivalent to removing the element.
Since you use data templating, the usual procedure would be to filter the source collection based on your condition to remove the data model. Removing the data model will result in the corresponding container (defined by the DataTemplate) not being rendered i.e. it will be hidden.
In WPF you always focus on the data models and not on their containers. It makes thinks a lot easier.
In the view model that defines the ParentModel property, you can define the filter by accessing the ICollectionView of the source collection:
private void ApplyFilter()
{
ICollectionView parentModelView = CollectionViewSource.GetDefaultView(this.ParentModel);
// Remove all items from the view that satisfy the condition
parentModelView.Filter = Predicate;
}
private bool Predicate(object collectionItem)
{
// TODO::Implement condition: return 'true' to hide the item from the view
}
If you want to filter the view directly (not recommended) you can access the ItemsControl.Items property to obtain the current ICollectionView:
private void OnMainWIndowLoaded(object sender, EventArgs e)
{
this.TabControl.Items.Filter = Predicate;
}
private bool Predicate(object collectionItem)
{
// TODO::Implement condition: return 'true' to hide the item from the view
}
Can't you simply control it on the element?
<TabControl>
<TabItem Header="Tabitem 1" Visibility="{Binding TabItem1Visibility, Converter={StaticResource BoolToVisibilityConverter}}"/>
<TabItem Header="Tabitem 2" />
<TabItem Header="Tabitem 3" />
</TabControl>
where TabItem1Visibility is a property in your VM:
private bool _tabItem1Visibility;
public bool TabItem1Visibility
{
get { return _tabItem1Visibility; }
set
{
_tabItem1Visibility = value;
OnPropertyChanged();
}
}
Notes:
BoolToVisibilityConverter's Convert method must return Collapse in case the value is false, not Hidden.
If you set TabItem1Visibility to false while the tab is selected, the content will still shown up, so you have to add SelectedItem="{Binding SelectedTab, Mode=TwoWay}" to TabControl and update it properly.

Deselecting ListView Item if already same item selected WPF

I had a ListView collection in WPF.
Let's say items were Apple, Banana, Cherry.
Assume user selected Banana at first.
If again user clicks on Banana the item was still selected.
I want to deselect the item on user click, If the same item was already selected.
Set the SelectionMode to Multiple or handle the PreviewMouseLeftButtonDown event for the ListViewItem containers depending on whether you want to be able to select several items or not:
private void ListViewItem_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
ListViewItem lvi = (ListViewItem)sender;
if (lvi.IsSelected)
{
e.Handled = true;
lvi.IsSelected = false;
}
}
XAML:
<ListView xmlns:s="clr-namespace:System;assembly=System.Runtime">
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<EventSetter Event="PreviewMouseLeftButtonDown"
Handler="ListViewItem_PreviewMouseLeftButtonDown" />
</Style>
</ListView.ItemContainerStyle>
<s:String>Apple</s:String>
<s:String>Banana</s:String>
<s:String>Cherry</s:String>
</ListView>

Reusable style for DataGridTextColumn in WPF [duplicate]

I tried to create a Style for DataGridTextColumn with the following code
<Style TargetType="{x:Type DataGridTextColumn}">
...
</Style>
However, Visual Studio 2010 highlights {x:Type DataGridTextColumn} with a blue line and elaborates: Exception has been thrown by the target of an invocation.
Why does this happen and how do I fix it?
You can't style the DataGridTextColumn because DataGridTextColumn does not derive from FrameworkElement (or FrameworkContentElement). Only FrameworkElement, etc supports styling.
When you attempt to create a style in XAML for any type that is not a FrameworkElement or FrameworkContentElement you get that error message.
How do you solve this? As with any problem, where there is a will there is a way. In this case I think the easiest solution is to create an attached property for DataGrid to assign a DataGridColumn style:
<DataGrid ...>
<local:MyDataGridHelper.TextColumnStyle>
<Style TargetType="FrameworkElement">
... setters here ...
</Style>
</local:MyDataGridHelper.TextColumnStyle>
...
The implementation would be something along these lines:
public class MyDataGridHelper : DependencyObject
{
// Use propa snipped to create attached TextColumnStyle with metadata:
... RegisterAttached("TextColumnStyle", typeof(Style), typeof(MyDataGridHelper), new PropertyMetadata
{
PropertyChangedCallback = (obj, e) =>
{
var grid = (DataGrid)obj;
if(e.OldValue==null && e.NewValue!=null)
grid.Columns.CollectionChanged += (obj2, e2) =>
{
UpdateColumnStyles(grid);
}
}
}
private void UpdateStyles(DataGrid grid)
{
var style = GetTextColumnStyle(grid);
foreach(var column in grid.Columns.OfType<DataGridTextColumn>())
foreach(var setter in style.Setters.OfType<Setter>())
if(setter.Value is BindingBase)
BindingOperations.SetBinding(column, setter.Property, setter.Value);
else
column.SetValue(setter.Property, setter.Value);
}
}
The way this works is, any time the attached property is changed, a handler is added for the Columns.CollectionChanged event on the grid. When the CollectionChanged event fires, all columns are updated with the style that was set.
Note that the above code does not handle the situation where a style is removed and re-added gracefully: Two event handlers are registered. For a really robust solution you would want to fix this by adding another attached property containing the event handler so the event handler could be unregistered, but for your purpose I think this is unimportant.
Another caveat here is that the direct use of SetBinding and SetValue will cause the DependencyProperty to have a BaseValueSource of Local instead of DefaultStyle. This will probably make no difference in your case but I thought I should mention it.
The style tag has to go in the right place. Your datagrid may look something like this right now:
<DataGrid>
<DataGrid.Columns>
<DataGridTextColumn />
</DataGrid.Columns>
</DataGrid>
You might initially try to add the style tag directly within the DataGridTextColumn element which will not work. You can however create elements for "DataGridTextColumn.ElementStyle" and or "DataGridTextColumn.EditingElementStyle" just within the "DataGridTextColumn" element. Each of those element tags can then have style tags within them:
<DataGrid>
<DataGrid.Columns>
<DataGridTextColumn>
<DataGridTextColumn.ElementStyle>
<Style TargetType="TextBlock">
<Setter Property="Background" Value="Green"></Setter>
</Style>
</DataGridTextColumn.ElementStyle>
<DataGridTextColumn.EditingElementStyle>
<Style TargetType="TextBox">
<Setter Property="Background" Value="Orange"></Setter>
</Style>
</DataGridTextColumn.EditingElementStyle>
</DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
One style will be applied to viewing and the other will be applied when the cell is in edit mode. Note that it changes from a TextBlock when viewing to a TextBox when editing (This got me at first!).
This is more an addition to Ray Burns answer. I first wasn't able to implement it on my own, but with help of mm8 (https://stackoverflow.com/a/46690951/5381620) I got it running. Works really fine. For other people who have problems following this attached property approach maybe a full code snippet is helpful.
public class MyDataGridHelper : DependencyObject
{
private static readonly DependencyProperty TextColumnStyleProperty = DependencyProperty.RegisterAttached("TextColumnStyle", typeof(Style), typeof(MyDataGridHelper), new PropertyMetadata
{
PropertyChangedCallback = (obj, e) =>
{
var grid = (DataGrid)obj;
if (e.OldValue == null && e.NewValue != null)
grid.Columns.CollectionChanged += (obj2, e2) =>
{
UpdateColumnStyles(grid);
};
}
});
public static void SetTextColumnStyle(DependencyObject element, Style value)
{
element.SetValue(TextColumnStyleProperty, value);
}
public static Style GetTextColumnStyle(DependencyObject element)
{
return (Style)element.GetValue(TextColumnStyleProperty);
}
private static void UpdateColumnStyles(DataGrid grid)
{
var origStyle = GetTextColumnStyle(grid);
foreach (var column in grid.Columns.OfType<DataGridTextColumn>())
{
//may not add setters to a style which is already in use
//therefore we need to create a new style merging
//original style with setters from attached property
var newStyle = new Style();
newStyle.BasedOn = column.ElementStyle;
newStyle.TargetType = origStyle.TargetType;
foreach (var setter in origStyle.Setters.OfType<Setter>())
{
newStyle.Setters.Add(setter);
}
column.ElementStyle = newStyle;
}
}
}
xaml
<Grid>
<DataGrid Name="MyDataGrid" ItemsSource="{Binding Lines}" AutoGenerateColumns="False" >
<local:MyDataGridHelper.TextColumnStyle>
<Style TargetType="TextBlock">
<Setter Property="TextWrapping" Value="Wrap"/>
</Style>
</local:MyDataGridHelper.TextColumnStyle>
<DataGrid.Columns>
<DataGridTextColumn Header="ProductId1" Binding="{Binding Path=Result1}" />
<DataGridTextColumn Header="ProductId2" Binding="{Binding Path=Result2}" />
</DataGrid.Columns>
</DataGrid>
</Grid>
Edit: In first approach I did overwrite the whole style. In new version it is still possible to maintain other styles modifications like this one
<DataGridTextColumn.ElementStyle>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Foreground" Value="Red"/>
</Style>
</DataGridTextColumn.ElementStyle>
A small addition to pedrito answer. Everything works fine, but if origStyle has base style, base style's setters get discarded.
To fix this, we need get all setters:
private static void UpdateColumnStyles(DataGrid grid)
{
var origStyle = GetTextColumnStyle(grid);
foreach (var column in grid.Columns.OfType<DataGridTextColumn>())
{
//may not add setters to a style which is already in use
//therefore we need to create a new style merging
//original style with setters from attached property
var newStyle = new Style();
newStyle.BasedOn = column.ElementStyle;
newStyle.TargetType = origStyle.TargetType;
var baseSetters = GetBaseSetters(origStyle);
var allSetters = baseSetters.Concat(origStyle.Setters.OfType<Setter>());
foreach (var setter in allSetters)
{
newStyle.Setters.Add(setter);
}
column.ElementStyle = newStyle;
}
}
private static IEnumerable<Setter> GetBaseSetters(Style style)
{
return style.BasedOn?.Setters.OfType<Setter>().Concat(GetBaseSetters(style.BasedOn)??new Setter[0]);
}
More simple:
<FontFamily x:Key="DefaultFont">Snap ITC</FontFamily>
<Style x:Key="ControlStyle" TargetType="Control">
<Setter Property="FontFamily" Value="{StaticResource DefaultFont}"/>
</Style>
<Style TargetType="{x:Type DataGridCellsPresenter}" BasedOn="{StaticResource ControlStyle}">
</Style>
A DataGridTextColumn is nothing but a column with a TextBlock in it. Write a style with the TargetType as TextBlock and bind the ElementStyle property of the DataGridTextColumn to it. Hope that helps!

XAML trigger setter and property of UIElementCollection type

My control has property Buttons of type UIElementCollection. Is it possible to modify such property via triggers (specifically DataTrigger)?
I have following code:
<Setter Property="Buttons">
<Setter.Value>
<Button>A</Button>
<Button>B</Button>
</Setter.Value>
</Setter>
And I get error "The property value is set more than once". Wrapping the buttons in UIElementCollection tag doesn't work (UIElementCollection has no default contructor). If I remove the second button, I get exception that the Buttons property is not compatible with type Button.
Thanks for any help
You can use an attached behavior to modify a collection with a setter. Here is a working example based on the Panel.Children property which is also a UIElementCollection:
<Grid>
<Grid.Resources>
<Style x:Key="twoButtons" TargetType="Panel">
<Setter Property="local:SetCollection.Children">
<Setter.Value>
<x:Array Type="UIElement">
<Button Content="Button1"/>
<Button Content="Button2"/>
</x:Array>
</Setter.Value>
</Setter>
</Style>
</Grid.Resources>
<StackPanel Style="{StaticResource twoButtons}"/>
</Grid>
And here is the attached property SetCollection.Children:
public static class SetCollection
{
public static ICollection<UIElement> GetChildren(DependencyObject obj)
{
return (ICollection<UIElement>)obj.GetValue(ChildrenProperty);
}
public static void SetChildren(DependencyObject obj, ICollection<UIElement> value)
{
obj.SetValue(ChildrenProperty, value);
}
public static readonly DependencyProperty ChildrenProperty =
DependencyProperty.RegisterAttached("Children", typeof(ICollection<UIElement>), typeof(SetCollection), new UIPropertyMetadata(OnChildrenPropertyChanged));
static void OnChildrenPropertyChanged(object sender, DependencyPropertyChangedEventArgs e)
{
var panel = sender as Panel;
var children = e.NewValue as ICollection<UIElement>;
panel.Children.Clear();
foreach (var child in children) panel.Children.Add(child);
}
}
Edit: A workaround would be using a converter, define your Buttons in a list in some resources:
<col:ArrayList x:Key="Buttons">
<Button>A</Button>
<Button>B</Button>
</col:ArrayList>
Namespace: xmlns:col="clr-namespace:System.Collections;assembly=mscorlib"
And use a custom converter in the setter to turn it into a collection:
<Setter Property="Buttons" Value="{Binding Source={StaticResource Buttons}, Converter={StaticResource ListToUIElementCollectionConverter}}"/>
Edit: Getting this to work properly is not a trivial task since the converter needs to know the parent object for the UIElementCollection-constructor.
In the end, I decided to circumvent the issue by modifying (with triggers) individual items of the collection (individual buttons) instead of changing whole collection.
I just hide and show the buttons depending on some conditions.

WPFToolkit DataGrid: Combobox column does not update selectedvaluebinding immediately

I'm using WPF Toolkit DataGrid and DataGridComboBoxColumn. Everything works well, except that when selection change happens on the combobox, the selectedvaluebinding source is not updated immediately. This happens only when the combobox loses focus. Has anyone run into this issue and any suggestions solutions ?
Here's the xaml for the column:
<toolkit:DataGridComboBoxColumn Header="Column" SelectedValueBinding="{Binding Path=Params.ColumnName, UpdateSourceTrigger=PropertyChanged}"
DisplayMemberPath="cName"
SelectedValuePath="cName">
<toolkit:DataGridComboBoxColumn.ElementStyle>
<Style TargetType="ComboBox">
<Setter Property="ItemsSource" Value="{Binding Info.Columns}" />
</Style>
</toolkit:DataGridComboBoxColumn.ElementStyle>
<toolkit:DataGridComboBoxColumn.EditingElementStyle>
<Style TargetType="ComboBox">
<Setter Property="ItemsSource" Value="{Binding Info.Columns}" />
</Style>
</toolkit:DataGridComboBoxColumn.EditingElementStyle>
</toolkit:DataGridComboBoxColumn>
UpdateSourceTrigger=PropertyChanged option is crucial here, it doesn't do without it.
The problem is that the cell remains in Edit mode until you leave the cell and the changes are committed
Solution: you need to create your own column type to override the default behavior
code:
public class AutoCommitComboBoxColumn : Microsoft.Windows.Controls.DataGridComboBoxColumn
{
protected override FrameworkElement GenerateEditingElement(Microsoft.Windows.Controls.DataGridCell cell, object dataItem)
{
var comboBox = (ComboBox)base.GenerateEditingElement(cell, dataItem);
comboBox.SelectionChanged += ComboBox_SelectionChanged;
return comboBox;
}
public void ComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
CommitCellEdit((FrameworkElement)sender);
}
}

Resources