How do I show either an image or text in a GridView cell (or bind an image to a certain cell's tooltip) depending on the data type? - wpf

I have a dictionary of string -> object, where the object can hold several value types such as a string or a bitmap
I've successfully bound the dictionary to a DataGrid, but I want to convert the bitmap values so that they'd display the image (either in-cell, or in a tooltip when hovering over the cell) instead of saying "System.Drawing.Bitmap".
Here's my code:
public class MyView : INotifyPropertyChanged
{
public MyView() {
Data = new Dictionary<string, object>();
Data["name 1"] = "value 1";
Data["name 2"] = new Bitmap("C:\\test.bmp");
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Data"));
}
public event PropertyChangedEventHandler PropertyChanged;
public IDictionary<string, object> Data { set; get; }
}
<Window x:Class="WpfApp1.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:src="clr-namespace:WpfApp1"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<src:MyView x:Key="myView"/>
<src:EnumerableConverter x:Key="enumerableConverter"/>
</Window.Resources>
<Grid>
<StackPanel>
<DataGrid HorizontalAlignment="Stretch" AutoGenerateColumns="True" VerticalAlignment="Stretch"
ItemsSource="{Binding Source={StaticResource myView}, Path=Data}" ToolTip="{Binding Source={RelativeSource AncestorType=AccessText}}">
</DataGrid>
</StackPanel>
</Grid>
</Window>
I'm not sure how to go about this. I've seen it applied to a whole column:
<DataGrid.Columns>
<DataGridTemplateColumn Header="Image" Width="SizeToCells" IsReadOnly="True">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Image Source="{Binding Image}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
But in my case, the same column would display both text and images.
And I know how to set the tooltip for the whole datagrid, but how do I make it change depending on cell content? And how do I make it display an image?

Related

Incorrect AlternationIndex when adding to an empty collection by entering a new DataGrid row

A simple example to reproduce the problem:
namespace IncorrectAlternativeIndex
{
/// <summary>A simple class to demonstrate the problem</summary>
public class Point2D
{
public double X { get; set; }
public double Y { get; set; }
}
}
namespace IncorrectAlternativeIndex
{
/// <summary>A simple ViewModel to demonstrate the problem</summary>
public class PointsViewModel : ViewModelBase
{
public ObservableCollection<Point2D> Points { get; } = new();
public RelayCommand ClearPoints => GetCommand(Points.Clear);
/// <summary>This command should not exist.
/// I brought it only to demonstrate the problem. </summary>
public RelayCommand Refresh => GetCommand<CollectionView>(cv => cv.Refresh());
}
}
<Window x:Class="IncorrectAlternativeIndex.CheckAlternativeIndexWindow"
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:IncorrectAlternativeIndex"
xmlns:sys="clr-namespace:System;assembly=netstandard"
mc:Ignorable="d"
Title="CheckAlternativeIndexWindow" Height="450" Width="800"
DataContext="{DynamicResource vm}">
<Window.Resources>
<local:PointsViewModel x:Key="vm"/>
</Window.Resources>
<UniformGrid Rows="1">
<DataGrid x:Name="dGrid" ItemsSource="{Binding Points}"
AlternationCount="{x:Static sys:Int32.MaxValue}">
<DataGrid.RowHeaderTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=(ItemsControl.AlternationIndex),
RelativeSource={RelativeSource FindAncestor,
AncestorType=DataGridRow}}"/>
</DataTemplate>
</DataGrid.RowHeaderTemplate>
</DataGrid>
<UniformGrid Columns="1">
<Button Content="Clear" Margin="50" Command="{Binding ClearPoints}"/>
<Button Content="Refresh" Margin="50" Command="{Binding Refresh}"
CommandParameter="{Binding Items, ElementName=dGrid}"/>
</UniformGrid>
</UniformGrid>
</Window>
When adding elements to an empty collection by typing into a new row directly in the DataGrid, the AlternationIndex value starts at AlternationCount - 1.
If do a Refresh of the collection view, the correct AlternationIndex calculation is restored.
After cleaning the collection, everything is repeated in the same way.
Is there a way to fix this?
P.S. Just in case, I clarify that I do not need line numbers in the source data. It is required only in the View for the convenience of the user.

It is show the name of the class when I use a template in a combobox

I want to update the text of an item in a combobox when the item is saved.
Although the entity that I use as source implements the notify property changed event, it is not update. I have read the solution it is to use a data templeate instead of DisplayMemberPath.
So I am using this:
<DataTemplate x:Key="TipoComponenteTemplate"
DataType="{x:Type clases:TiposComponentesUI}">
<TextBlock Text="{Binding TipoComponente}"/>
</DataTemplate>
<ComboBox Name="cmbTiposComponentes" Width="150"
Text="{Binding TiposComponentesTexto, UpdateSourceTrigger=PropertyChanged}"
ItemsSource="{Binding TiposComponentes}"
ItemTemplate="{StaticResource ResourceKey=TipoComponenteTemplate}"
SelectedItem="{Binding TiposComponentesSelectedItem}">
That works partially, in the way when I open the combobox, the items show the correct text, but when I select one of them, in the textbox of the combobox it is shown the name of the class instead of the data in the property TipoComponente that is defined in the data template.
So I would like to know how I could show the same text in the textbox than the text that is shown in the items.
Thanks.
I've reproduced this scenario and both the the ItemTemplate and the DisplayMemberPath solutions work fine for me, as long as the ComboBox's IsEditable property is set to False.
When IsEditable="False"
Here's what I have:
Book.cs
using CommunityToolkit.Mvvm.ComponentModel;
namespace WpfApp2;
public partial class Book : ObservableObject
{
[ObservableProperty]
private string? _author;
[ObservableProperty]
private string? _title;
}
MainViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System;
using System.Collections.ObjectModel;
namespace WpfApp2;
public partial class MainViewModel : ObservableObject
{
public ObservableCollection<Book> Books { get; } = new();
[ObservableProperty]
private Book? _selectedBook;
public MainViewModel()
{
Books.Add(new Book { Author = "Jim Weasley", Title = "The mystery of eggplants" });
Books.Add(new Book { Author = "Ryan Reynolds", Title = "Doplhins and how to beat them" });
Books.Add(new Book { Author = "Sarah O'Connel", Title = "What is djent?" });
}
[RelayCommand]
private void UpdateRandomBookTitles()
{
foreach (var book in Books)
{
book.Title = Guid.NewGuid().ToString();
}
}
}
MainWindow.xaml.cs
<Window
x:Class="WpfApp2.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:local="clr-namespace:WpfApp2"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="MainWindow"
Width="800"
Height="450"
mc:Ignorable="d">
<Window.DataContext>
<local:MainViewModel />
</Window.DataContext>
<Window.Resources>
<DataTemplate x:Key="BookDisplayTemplate" DataType="{x:Type local:Book}">
<TextBlock Text="{Binding Title}" />
</DataTemplate>
</Window.Resources>
<StackPanel>
<Button Command="{Binding UpdateRandomBookTitlesCommand}" Content="Randomize book titles" />
<ComboBox
ItemTemplate="{StaticResource BookDisplayTemplate}"
ItemsSource="{Binding Books}"
SelectedItem="{Binding SelectedBook}" />
<!-- This will work as well -->
<!-- <ComboBox
DisplayMemberPath="Title"
ItemsSource="{Binding Books}"
SelectedItem="{Binding SelectedBook}" />-->
</StackPanel>
</Window>
After you've selected a book, changing it's Title will be reflected in ComboBox's Text as well.
The problem: when IsEditable="True"
Once you set IsEditable="True", this stops working and I believe it's intentional. When IsEditable="True", the ComboBox's Text is only update when:
an item is selected
user types text manually
Changing the SelectedBook's Title does nothing here, my guess is so that it doesn't overwrite user input. Only selected another item will do that.

dataBinding a UserControl to data in DataGrid [duplicate]

This question already has answers here:
DataGridTemplateColumn Two way binding is not working
(2 answers)
Closed 3 years ago.
I have a lilltle issue using Dependency Property for binding data;
I have a user control which contains a textBox. I created a DataGrid, in which I added the usercontrol for one of its columns. Then i did a binding using dataContext.
When I first run the apllication, the usercontrol is filled with data (Data binded to a specific field 'name' in my case), But when I modify the value, the new value is not written, it still holds the old value.
TT.xaml.cs
namespace WPF_Prj.View
{
public partial class TT : UserControl
{
public float MyText
{
get {
return (float)GetValue(MyTextProperty);
}
set {
SetValue(MyTextProperty, value );
}
}
public static readonly DependencyProperty MyTextProperty =
DependencyProperty.Register("MyText", typeof(float), typeof(TT), new FrameworkPropertyMetadata(null) { BindsTwoWayByDefault = true });
public TT()
{
InitializeComponent();
}
}
}
TT.xaml
<UserControl x:Class="WPF_Prj.View.TT"
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">
<Grid>
<TextBox Height="20" Width="100"
Text="{Binding RelativeSource={RelativeSource AncestorType=UserControl},
Path=MyText,
UpdateSourceTrigger=PropertyChanged}">
</TextBox>
</Grid>
mainWindow.xaml
<Window x:Class="WPF_Prj.View.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:col="clr-namespace:System.Collections;assembly=mscorlib"
xmlns:local="clr-namespace:WPF_Prj.View"
Title="[Portfolio] MainWindow" Height="500" Width="800">
<StackPanel Orientation="Vertical">
<DataGrid x:Name="datagrid" AutoGenerateColumns="False" CanUserAddRows="False"
Width="Auto" Margin="5,5,0,5" HorizontalAlignment="Left" CellEditEnding="datagrid_CellEndEditing">
<DataGrid.Columns>
<DataGridTextColumn Header="" IsReadOnly="True" Width="1"/>
<DataGridTextColumn Header="Name" Binding="{Binding Name, Mode=TwoWay, NotifyOnTargetUpdated=True}" IsReadOnly="True" Width="Auto"/>
<DataGridTextColumn Header="Owned Qty" Binding="{Binding OwnedQty, Mode=TwoWay, NotifyOnTargetUpdated=True}" IsReadOnly="True" Width="Auto"/>
<DataGridTemplateColumn Header="Ordered/Eligible">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<local:TT x:Name="test" MyText="{Binding OwnedQty, Mode=TwoWay}"></local:TT>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</StackPanel>
in the mainWindow.xaml.cs I get data from a ViewModel class which implements ObservableCollection and the Model is a class implementing INotifyPropertyChanged
So in the picture below : the green box is what is loaded from my DB to the grid, in the red is my UserControl which hold and edit, at first it contains the values loaded, but when I change it to some other values, the values in the Green still unchanged.
This fixes it for me. You don't need Mode=TwoWay; it's already that by default in the dependency property definition. You do need UpdateSourceTrigger=PropertyChanged, whether that's in the DP definition or not.
<DataTemplate>
<local:TT
x:Name="test"
MyText="{Binding OwnedQty, UpdateSourceTrigger=PropertyChanged}"
></local:TT>
</DataTemplate>
I would expect to be able to do the same thing by initializing DefaultUpdateSourceTrigger in the Dependency Property definition, but this does not have the same effect. I don't know why that is the case.
Note that I also changed the default value to 0F; null will throw an exception.
public static readonly DependencyProperty MyTextProperty =
DependencyProperty.Register("MyText", typeof(float), typeof(TT),
new FrameworkPropertyMetadata(0F) {
BindsTwoWayByDefault = true
// Nope.
//, DefaultUpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
});
This is my viewmodel property. I'm putting a breakpoint on OnPropertyChanged() to detect when the property is updated by the binding.
private float _ownedQty = 0F;
public float OwnedQty
{
get { return _ownedQty; }
set
{
if (value != _ownedQty)
{
_ownedQty = value;
OnPropertyChanged();
}
}
}

How to bind multiple ComboBoxes to one Collection? [duplicate]

This question already has an answer here:
WPF ComboBox bind itemssource to different datacontext in MVVM
(1 answer)
Closed 4 years ago.
I Need a collection of ComboBoxes with a common collection of possible selections.
Codebehind excerpt:
namespace ComboBoxesInCollection
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
D = new DataContainer();
this.DataContext = D;
}
private DataContainer D;
}
public class DataContainer
{
public ObservableCollection<Item> ComboboxItems
{
get { return comboboxItems; }
}
public ObservableCollection<Selection> ComboboxSelections
{
get { return comboboxSelections; }
}
public DataContainer()
{
comboboxItems = new ObservableCollection<Item>
{
new Item(100, "Entry #1"),
new Item(101, "Entry #2"),
new Item(102, "Entry #3")
};
comboboxSelections = new ObservableCollection<Selection>()
{
new Selection(1),
new Selection(2),
new Selection(3)
};
}
private ObservableCollection<Item> comboboxItems;
private ObservableCollection<Selection> comboboxSelections;
}
}
XAML excerpt:
<Window.Resources>
<DataTemplate x:Key="CSTemplate">
<Border BorderBrush="Black" BorderThickness="1,1,0,0" Margin="0,0,0,4" Padding="4">
<StackPanel>
<Label Content="{Binding Id}"/>
<ComboBox
ItemsSource="{Binding ComboboxItems}" //<= does not work
DisplayMemberPath="Name"
SelectedValue="{Binding SelectedId}"
SelectedValuePath="Id"
/>
</StackPanel>
</Border>
</DataTemplate>
...
<ItemsControl ItemsSource="{Binding ComboboxSelections}" ItemTemplate="{StaticResource CSTemplate}"/>
The ItemsControl shows the items, but the Combobox is empty.
I know that i try to access a property/collection inside of a Selection that is not existing right now.
How would i correctly specify the DataBinding so i can see the items?
Used Element Binding to access the DataContext Property of the window.
First Name the Window x:Name="Window1" and the while binding, use element binding.
ItemsSource="{Binding ElementName=Window1, Path=DataContext.ComboboxItems}"
XAML of whole window
<Window x:Class="WpfApp6.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:WpfApp6"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800" x:Name="Window1">
<Window.Resources>
<DataTemplate x:Key="CSTemplate">
<Border BorderBrush="Black" BorderThickness="1,1,0,0" Margin="0,0,0,4" Padding="4">
<StackPanel>
<Label Content="{Binding Id}"/>
<ComboBox
ItemsSource="{Binding ElementName=Window1, Path=DataContext.ComboboxItems}"
DisplayMemberPath="Name"
SelectedValue="{Binding SelectedId}"
SelectedValuePath="Id"
/>
</StackPanel>
</Border>
</DataTemplate>
</Window.Resources>
<Grid>
<ItemsControl ItemsSource="{Binding ComboboxSelections}" ItemTemplate="{StaticResource CSTemplate}" />
</Grid>

WPF DataGrid with only one combobox in templatecolumn not adding new rows

I'm completely new to WPF so please apologise any "stupid" mistakes.
I have a datagrid with only one column, that is a combobox. The datagrid shows a new empty line as expected. But if I select a value in the combobox on the new line, no additional new row is added. I already tried to add an edit template according to this answer: datagrid showing one new row, but not any subsequent but that did no help.
<Window x:Class="WPFSpielplatz.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="clr-namespace:WPFSpielplatz"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<d:MainWindowViewModel />
</Window.DataContext>
<Grid>
<StackPanel HorizontalAlignment="Left" Height="319" VerticalAlignment="Top" Width="517">
<DataGrid AutoGenerateColumns="False" ItemsSource="{Binding GroceryItems}" CanUserAddRows="True">
<DataGrid.Columns>
<DataGridTemplateColumn Header="Combo">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding RelativeSource={RelativeSource AncestorType=DataGrid}, Path=DataContext.GroceryItemTypes}" SelectedItem="{Binding GroceryItemType, UpdateSourceTrigger=PropertyChanged}" DisplayMemberPath="Name" ></ComboBox>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding RelativeSource={RelativeSource AncestorType=DataGrid}, Path=DataContext.GroceryItemTypes}" SelectedItem="{Binding GroceryItemType, UpdateSourceTrigger=PropertyChanged}" DisplayMemberPath="Name" ></ComboBox>
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</StackPanel>
</Grid>
My ViewModel:
using System.Collections.ObjectModel;
using System.ComponentModel;
using WPFSpielplatz.Annotations;
namespace WPFSpielplatz
{
public class MainWindowViewModel:INotifyPropertyChanged
{
private ObservableCollection<GroceryItem> _groceryItems;
public MainWindowViewModel()
{
GroceryItemTypes = new ObservableCollection<GroceryItemType>
{
new GroceryItemType("Food"),
new GroceryItemType("Non-Food")
};
_groceryItems=new ObservableCollection<GroceryItem>
{
new GroceryItem(){GroceryItemType=GroceryItemTypes[0]},
new GroceryItem(){GroceryItemType=GroceryItemTypes[1]}
};
}
public ObservableCollection<GroceryItem> GroceryItems
{
get { return _groceryItems; }
set
{
_groceryItems = value;
OnPropertyChanged("GroceryItems");
}
}
public ObservableCollection<GroceryItemType> GroceryItemTypes { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
The "domain" classes:
GroceryItem:
namespace WPFSpielplatz
{
public class GroceryItem
{
public GroceryItemType GroceryItemType { get; set; }
public GroceryItem()
{
}
}
}
GroceryItemType:
namespace WPFSpielplatz
{
public class GroceryItemType
{
public GroceryItemType()
{
}
public GroceryItemType(string name)
{
Name = name;
}
public string Name { get; set; }
}
}
<DataGrid AutoGenerateColumns="False" ItemsSource="{Binding GroceryItems}"/>
is simply enough for binding your collection to the DataGrid. It is already relative, however you just need to bind your properties of the GroceryItem class to your DataGrid as follows:
<ComboBox ItemsSource="{Binding GroceryItemType.Name, Mode=TwoWay"}/>
This will insert the name for every GroceryItemType to the associated GroceryItem in a ComboBox. You will probably end up in having just the name of the GroceryItemType in the ComboBox since you did specify this as your ItemsSource. I don't think that is what you want but for now, I don't see any more code in order to understand, what you want to archive.
Please note that you will have to implement INotifyPropertyChanged in both Models (GroceryItem and GroceryItemType) if you want to reflect the changes to the ViewModel and vice versa. This is where the Mode=TwoWay comes in handy.
Ok, I solved it. It worked all the time, the problem was, that my <DataGridTemplateColumn.CellTemplate> and my <DataGridTemplateColumn.CellEditingTemplate> looked the same so I could change values in the combobox without ever entering editing mode. But only after entering and successfully leaving editing mode a new row is added to the datagrid.
To get this working you have to click multiple times in the cell you want to edit and then tab out of it for the new row to be added.
So the updated code for the View looks like this:
<Window x:Class="WPFSpielplatz.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="clr-namespace:WPFSpielplatz"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<d:MainWindowViewModel />
</Window.DataContext>
<Grid>
<StackPanel HorizontalAlignment="Left" Height="319" VerticalAlignment="Top" Width="517">
<DataGrid AutoGenerateColumns="False" ItemsSource="{Binding GroceryItems}" CanUserAddRows="True">
<DataGrid.Columns>
<DataGridTemplateColumn Header="Combo">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Path=GroceryItemType.Name}" ></TextBlock>
</StackPanel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<StackPanel>
<TextBlock>Edit</TextBlock>
<ComboBox ItemsSource="{Binding RelativeSource={RelativeSource AncestorType=DataGrid}, Path=DataContext.GroceryItemTypes}" SelectedItem="{Binding GroceryItemType, UpdateSourceTrigger=PropertyChanged}" DisplayMemberPath="Name" ></ComboBox>
</StackPanel>
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</StackPanel>
</Grid>

Resources