WPF listbox won't show images wrapped in multiple columns - wpf

I am trying to get the content of a directory with many images displayed in a listbox wrapping the content horizontally, showing pics small and resizable. There's a sample on http://msdn.microsoft.com/en-us/library/ms771331%28v=vs.85%29.aspx which is doing just that, but it takes 10 seconds to build with 2500 pictures while I want it filled dynamically and also it uses thumbnails which do not always seem to be stored in images.
I've tried to add a VirtualizingStackPanel without any visible change and lots more and finally built a new program from a very basic sample, see below. This shows content immediatly, also when I apply a size converter, but I can in no way get it in multiple columns! It seems that the Microsoft example gets this done by adding a WrapPanel to the a style which targets the listbox, and the line IsItemsHost="True" apparently is crucial to getting the images in multiple columns. When I try the same in my sample (in PhotoListBoxStyle), the program doesn't even start anymore. When I rebuild the program as the Microsoft example but keep the code behind to arrange the binding, it is still fast, but the resizer stops working properly and it still uses 1 column.
What can I do to get the below code wrapped in multiple columns?
Dick
XAML:
<Window x:Class="PhotoData.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:PhotoData"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<local:UriToBitmapConverter x:Key="UriToBitmapConverter" />
<!-- Main photo catalog view -->
<Style TargetType="{x:Type ListBox}" x:Key="PhotoListBoxStyle">
<Setter Property="Foreground" Value="White" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBox}" >
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<GroupBox Grid.Column="0" Grid.Row="1">
<ListBox Margin="10" Name="designerListBox" >
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal"
VerticalAlignment="Center"
HorizontalAlignment="Center"
IsItemsHost="True">
<Image Source="{Binding imageLocation, Converter={StaticResource UriToBitmapConverter}}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</GroupBox>
</Window>
Code behind:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
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 PhotoData
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
List<binderClass> myList = new List<binderClass>();
foreach (string file in Directory.GetFiles(#"c:\temp", "*.jpg", SearchOption.AllDirectories))
{
myList.Add(new binderClass() { imageLocation = file, displayName = "TEST" });
Debug.WriteLine(file);
}
designerListBox.ItemsSource = myList;
}
}
public class UriToBitmapConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
BitmapImage bi = new BitmapImage();
bi.BeginInit();
bi.DecodePixelWidth = 100;
bi.CacheOption = BitmapCacheOption.OnLoad;
bi.UriSource = new Uri(value.ToString());
bi.EndInit();
return bi;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new Exception("The method or operation is not implemented.");
}
}
class binderClass
{
public string imageLocation
{
get;
set;
}
public string displayName
{
get;
set;
}
}
}

If you want to wrap content of ListBox then you need to change ListBox.ItemsPanel, which hosts all items, to be WrapPanel and then your ItemTemplate will be just Image:
<ListBox Name="designerListBox" ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<Image Width="100" Source="{Binding imageLocation, Converter={StaticResource UriToBitmapConverter}}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
StackPanel or VirtualizingStackPanel will never wrap content. It will stack child item horizontally or vertically.
EDIT
You may hit some performance issues, especially when working with images, when using panel different then VirtualizingStackPanel. Other panels don't provide virtualization so all items are treated as if they are visible and will be loaded immediately.
The standard layout system creates item containers and computes layout for each item associated with a list control. The word "virtualize" refers to a technique by which a subset of user interface (UI) elements are generated from a larger number of data items based on which items are visible on-screen. Generating many UI elements when only a few elements might be on the screen can adversely affect the performance of your application. The VirtualizingStackPanel calculates the number of visible items and works with the ItemContainerGenerator from an ItemsControl (such as ListBox or ListView) to create UI elements only for visible items.

Related

Contents of Content Control gone on tab switch

I am using mvvm. I am loading a usercontrol that contains a content control on two different tabs like so:
<TabControl>
<TabItem Header="View">
<StackPanel>
<Info:UserData/><!--UserData Control-->
<Button Content="View Entries" Command="{Binding BeginView}"/>
</StackPanel>
</TabItem >
<TabItem Header="Edit">
<StackPanel>
<Info:UserData/><!--UserData Control-->
<Button Content="Edit Entries" Command="{Binding BeginEdit}"/>
</StackPanel>
</TabItem >
</TabControl>
The User Control looks like:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ContentControl Grid.Row="0" Content="{Binding UserTypeInfo}"/>
<Info:UserDetailsArea Grid.Row="1"/>
</Grid>
When the tab first loads the content of the ContentControl is set to an image. Depending on some actions the content may change to a datatable, video, etc. This part works fine.
When it loads the default tab is the first one. If I click on the second tab, you should see the same thing - with a different button, this works. But if I go back to the first tab the Content control is empty.
What do I need to do so that both tabs display the image?
The value bound to from the viewmodel as requested:
private object userTypeInfo
/// <summary>
/// User Specific data
/// </summary>
public object UserTypeInfo
{
get
{
return userTypeInfo;
}
private set
{
UuserTypeInfo= value;
OnPropertyChanged("UserTypeInfo");
}
}
Edit:
The following is a simplified example that I believe shows the same problem:
XAML code for window:
<Window x:Class="dualCC.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525"
Loaded="Window_Loaded">
<Grid>
<TabControl>
<TabItem Header="One">
<StackPanel>
<Button Content="One" />
<ContentControl Name="CCone"/>
</StackPanel>
</TabItem>
<TabItem Header="Two">
<StackPanel>
<Button Content="Two" />
<ContentControl Name="CCtwo"/>
</StackPanel>
</TabItem>
</TabControl>
</Grid>
</Window>
Code behind (you'll need to fix the path to an image):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
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 dualCC
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
Uri uri = new Uri(#"C:\Image.jpg");
BitmapImage temp = new BitmapImage(uri);
Image CurrentImage = new Image();
CurrentImage.Source = temp;
CCone.Content = CurrentImage;
CCtwo.Content = CurrentImage;
}
}
}
This isn't MVVM. In MVVM you never manipulate GUI elements directly in code-behind like this.
To answer your question, the problem is that you're creating an Image, which is actually a child control, and setting it as the content of two separate controls. Controls can only have one parent. Create separate Images instead and set the BitmapImage as the source for each:
private void Window_Loaded(object sender, RoutedEventArgs e)
{
Uri uri = new Uri(#"C:\Image.jpg");
BitmapImage temp = new BitmapImage(uri);
CCone.Content = new Image { Source = temp };
CCtwo.Content = new Image { Source = temp };
}
Or better yet use proper MVVM and do it with data binding.

Single Left Mouse Click on TreeView Node to open a document on a different User Control

I have Tree View whose information are filled with a structure of a document.
Every single article is represented by a single TreeView Node.
The goal is to raise the click event, pass the key that identifies that precise part of the document and render the information
I have 3 problems:
1) How can I pass the information to a different User Control
2) The Double click event works (just tried with a simple textbox) but not the single left click... :(
3) How can I open the precise part of the document I select on the treeview and repeat the operation. So e.g.: I click on the article number 3, I want the document of article 3 rendered, I click on article 5 etc. etc.
Code below:
<UserControl x:Class="UserControls.DocumentViewLaw"
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="800" d:DesignWidth="900"
xmlns:controls="clr-namespace:Client.UserControls">
<Grid x:Name="grdTop">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="220"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<StackPanel Grid.Column="0" Grid.ColumnSpan="2">
<TreeView x:Name="treeViewStructure" HorizontalAlignment="Left" Width="200" >
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<Border x:Name="bdrTreeViewItem" HorizontalAlignment="Right" BorderThickness="2" Margin="0.5" Padding="1">
<TreeViewItem Header="{Binding Text}" x:Name="treeViewItem" HorizontalAlignment="Left" HorizontalContentAlignment="Left">
</TreeViewItem>
</Border>
<HierarchicalDataTemplate.Resources>
<Style TargetType="{x:Type TreeViewItem}">
<EventSetter Event="MouseDoubleClick" Handler="OnTreeNodeMouseClick" />
</Style>
<Style TargetType="{x:Type Border}">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="BorderBrush" Value="LightBlue" />
<Setter Property="BorderThickness" Value="3" />
<Setter Property="CornerRadius" Value="9" />
</Trigger>
</Style.Triggers>
</Style>
</HierarchicalDataTemplate.Resources>
<HierarchicalDataTemplate.Triggers>
<Trigger SourceName="treeViewItem" Property="IsMouseOver" Value="True">
<Setter TargetName="bdrTreeViewItem" Property="Background" Value="LightGray" />
<Setter TargetName="treeViewItem" Property="Foreground" Value="Red" />
</Trigger>
</HierarchicalDataTemplate.Triggers>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</StackPanel>
<StackPanel Grid.Column="1" VerticalAlignment="Stretch" HorizontalAlignment="Stretch">
<controls:TabDocumentViewLawControl x:Name="topTabLaw" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Grid.Row="0" />
</StackPanel>
</Grid>
</UserControl>
CodeBehind:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
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 Client.UserControls
{
public partial class DocumentViewLaw : UserControl
{
public DocumentViewLaw()
{
InitializeComponent();
}
public void SetTreeViewNodeStructure(IList<TreeViewNode> nodes)
{
//this method is recalled in MainWindow.cs where I pass the object returned by
// WCF and attached to the TreeView
this.treeViewStructure.ItemsSource = nodes;
}
public void OnTreeNodeMouseClick(object sender, RoutedEventArgs e)
{
}
}
}
Second User Control where to visualize the document:
<UserControl x:Class="Client.UserControls.TabDocumentViewLawControl"
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:editor="clr-namespace:RichEditor;assembly=RichEditor"
mc:Ignorable="d"
d:DesignHeight="500" d:DesignWidth="500"
xmlns:vm="clr-namespace:Domain.Model.Document;assembly=Domain">
<UserControl.Resources>
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
</UserControl.Resources>
<ScrollViewer Grid.Row="5" Grid.Column="1" MaxHeight="250">
<StackPanel>
<FlowDocumentReader x:Name="articoloDocumentLaw" Grid.Row="1" Document="{Binding Path=FlowDocumentArticle}"
Visibility="{Binding Path=HasArticoloVisible, Converter={StaticResource BooleanToVisibilityConverter}}" />
</StackPanel>
</ScrollViewer>
</UserControl>
The object that I pass to the UserControl to visualize the document and his structure in
"DocumentViewLaw" User Control is the single result of a result list
In MainWindow component I associate the data and correspondant context.
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.Loaded += MainWindow_Loaded;
this.login.btnLogin.Click += btnLogin_Click;
this.tabMainControl.resultListControl.RowSelected += resultListControl_RowSelected;
}
void resultListControl_RowSelected(object sender, EventArgs e)
{
AutonomySearchResult selectedDocument = (AutonomySearchResult)this.tabMainControl.resultListControl.grdResult.SelectedItem;
this.tabMainControl.topTabControl.SelectedItem = this.tabMainControl.tabResultList;
Services.ServicesClient client = new Services.ServicesClient();
var document = client.GetDocument(selectedDocument.DocKey, true);
this.tabMainControl.topTabControl.SelectedItem = this.tabMainControl.tabDocumentView;
this.tabMainControl.tabDocumentView.DataContext = document;
TreeViewFactory treeFactory = new TreeViewFactory();
var documentStructure= treeFactory.GetStructure(document.DocumentKey, document.XmlStructure, true);
this.tabMainControl.documentViewLaw.SetTreeViewNodeStructure(documentStructure);
}
public virtual void onResultClick(object sender, RoutedEventArgs e)
{
}
}
Factory of TreeView:
public class TreeViewFactory
{
public IList GetStructure(DocumentKey docKey, string structure, bool loadAllParents)
{
//business logic with LINQ2XML
}
public class TreeViewNode
{
public TreeViewNode() { }
public DocumentKey DocKey { get; set; }
public string Text { get; set; }
public IList<TreeViewNode> Children { get; set; }
}
Thank u very much in advance :)
1) How can I pass the information to a different User Control?
I assume that the articles data in the TreeView.ItemSource and the data in the second UserControl are bound from properties in a view model or class that is set as the DataContext of your Window or UserControl. From your view model, you can either bind to the SelectedItem property of the TreeView, or monitor the INotifyPropertyChanged interface to see when the property that is bound to the TreeView.ItemsSource is changed (by the user). At this point, you can load the required data and update whichever property is data bound to the second UserControl.
2) The Double click event works (just tried with a simple textbox) but not the single left click... :(
If you data bind as suggested above, then you will not need a Click handler, as you can find out when the user selects another node directly in the view model.
3) How can I open the precise part of the document I select on the treeview and repeat the operation. So e.g.: I click on the article number 3, I want the document of article 3 rendered, I click on article 5 etc. etc.
I don't really understand this part of your question. You should be able to access all of the properties of the selected item in the TreeView when binding to the view model.

Multiple dataContext for one control - MVVM

I am not sure if my question header represent exactly my problem, I will do the best to explain:
I have a grid cell DataTemplate: (the grid belong to third party company but it`s not important for my question)
<DataTemplate>
<TextBlock>
<Hyperlink Command="{Binding OpenLinkCommand}">
<Hyperlink.ToolTip>
<TextBlock Text="{Binding Data.MapLink}"/>
</Hyperlink.ToolTip>
<TextBlock Text="{Binding Data.MapLink}" TextDecorations="underline">
</Hyperlink>
</TextBlock>
</DataTemplate>
I want make this DataTemplate to show some hyperlink ("Data.MapLink" is the object which contain the link value) and each click on this link will fire the command "OpenLinkCommand".
The problem is that "Data.MapLink" and "OpenLinkCommand" are located in different dataContext and then I have to choose one of the next choices:
leave hyperlink dataContext as it - the command won`t work and the hyperlink will get the Data.MapLink value.
change hyperlink dataContext to the command datacontext - The command will work but the hyperlink name will be empty.
Regretfully I don`t have option put those items in same dataContext so I must find a way how to tell the command that it dataContext is "X" and tell the hyperLink that it dataContext is "Y".
I am hoping that my question is clear
How can I solve this problem?
There are some binding properties you can use to specify a different Source for your binding than the default DataContext
The most common ones are ElementName or RelativeSource, which will find another UI element in the VisualTree so you can bind to it's properties.
For example, the following uses ElementName to tell the binding that it should use MyGridView as the binding source, and to bind to MyGridView.DataContext.OpenLinkCommand
<Hyperlink Command="{Binding ElementName=MyGridView,
Path=DataContext.OpenLinkCommand}">
You can also use RelativeSource in a binding to find an object further up the VisualTree of the specified object type, and use it as the binding source. This example does the same thing as the above example, except it uses RelativeSource instead of ElementName, so your GridView doesn't need to have a Name specified.
<Hyperlink Command="{Binding
RelativeSource={RelativeSource AncestorType={x:Type GridView}},
Path=DataContext.OpenLinkCommand}">
A third option is to set the binding's Source property to a static object, like this:
<Hyperlink Command="{Binding
Source={x:Static local:MyStaticClass.OpenLinkCommand}}">
Based on your comment here about binding to a singleton, this would probably be the best option for you.
You will have to have an instance of the desired data context (usually in the resources of a control or window). Once you have that, you should be able to explicitly set the data context of the textblock instead of inheriting the parent data context automatically.
For example:
<TextBlock DataContext="{StaticResource MyDataMapLinkDataContext}" Text="{Binding Data.MapLink}" TextDecorations="underline"/>
If you really do need to use another property for an extra data context then you can just use an attached property.
XAML
<Window.Resources>
<Style TargetType="Button">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<ContentPresenter Content="{Binding (local:ExtraDataContextProvider.ExtraDataContext), RelativeSource={RelativeSource TemplatedParent}}"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<Grid>
<Button Margin="172,122,131,79" Foreground="Green" local:ExtraDataContextProvider.ExtraDataContext="A test">
test
</Button>
</Grid>
</Window>
Code
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 WpfApplication1
{
public class ExtraDataContextProvider : DependencyObject
{
public static object GetExtraDataContext(DependencyObject obj)
{
return (object)obj.GetValue(ExtraDataContextProperty);
}
public static void SetExtraDataContext(DependencyObject obj, object value)
{
obj.SetValue(ExtraDataContextProperty, value);
}
public static readonly DependencyProperty ExtraDataContextProperty = DependencyProperty.RegisterAttached("ExtraDataContext", typeof(object), typeof(ExtraDataContextProvider), new PropertyMetadata(null));
}
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
}

Make first row of DataGrid ReadOnly

I'm using a DataGrid to display user permissions in my WPF-application.
The first row of the DataGrid will always contain the owner of the project for which the permissions are displayed.
This owner is set when the project is created and can not be changed directly from the DataGrid.
My question then is.
How can I make the first row ReadOnly, and maybe give it a specific style so the background can be changed?
You will need a trigger for this, the only problem is that getting a row index on the wpf datagrid is pretty awful, so i tend to do something like this:
<DataGrid.RowStyle>
<Style TargetType="DataGridRow">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=IsOwner}" Value="true">
<Setter Property="IsEnabled" Value="False"/>
</DataTrigger>
</Style.Triggers>
</Style>
</DataGrid.RowStyle>
where there's a property value on the object i know will be unique to that particular row. So if there's a unique ID or something that the owner always has, you can bind to that.
The setter property is 'IsEnabled' as datagridrow doesn't contain a property for readonly, but this will stop a user modifying the row.
Here's my take on it. The previous sample will do you just fine, my sample is for demonstration of the approach for acquiring low level control over the cells look. In WPF DataGridRow is just a logical container, you can only use 'attached' properties with it, such as Enabled, FontSize, FontWeight etc., as they'll get propagated down to the cell level), but the actual control's look is defined at a cell level.
TextBlock's for readonly stuff generally look cleaner than disabled texboxes, also you might want to apply completely different style for readonly and editable modes of your cells, for which you'll have to do somewhat similar to what the code below does.
Code:
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 ReadOnlyRows
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.Loaded += (o, e) =>
{
this.g.ItemsSource = new List<Person>(2)
{
new Person(){ Name="Dmitry", Role="Owner" },
new Person(){ Name="Jody", Role="BA" }
};
};
}
}
public class Person
{
public string Role
{
get;
set;
}
public string Name
{
get;
set;
}
}
public class PersonServices
{
// that shouldn't be in template selector, whould it?
public static bool CanEdit(Person person)
{
return person.Role != "Owner";
}
}
public class TemplateSelector : DataTemplateSelector
{
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
Person person = item as Person;
if (person == null) return null;
string templateName = PersonServices.CanEdit(person) ? "EditableDataTemplate" : "ReadOnlyDataTemplate";
return (DataTemplate)((FrameworkElement)container).FindResource(templateName);
}
}
public class EditingTemplateSelector : DataTemplateSelector
{
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
Person person = item as Person;
if (person == null) return null;
string templateName = PersonServices.CanEdit(person) ? "EditableEditingDataTemplate" : "ReadOnlyEditingDataTemplate";
return (DataTemplate)((FrameworkElement)container).FindResource(templateName);
}
}
}
XAML:
<Window x:Class="ReadOnlyRows.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ReadOnlyRows"
Title="MainWindow" Height="350" Width="525">
<Grid>
<DataGrid x:Name="g" AutoGenerateColumns="False"
CanUserAddRows="False">
<DataGrid.Resources>
<DataTemplate x:Key="EditableEditingDataTemplate">
<TextBox Text="{Binding Name}" />
</DataTemplate>
<DataTemplate x:Key="ReadOnlyEditingDataTemplate">
<TextBlock Text="{Binding Name}" FontWeight="Bold" />
</DataTemplate>
<DataTemplate x:Key="EditableDataTemplate">
<TextBlock Text="{Binding Name}" />
</DataTemplate>
<DataTemplate x:Key="ReadOnlyDataTemplate">
<TextBlock Text="{Binding Name}" FontWeight="Bold" />
</DataTemplate>
<local:TemplateSelector x:Key="TemplateSelector" />
<local:EditingTemplateSelector x:Key="EditingTemplateSelector" />
</DataGrid.Resources>
<DataGrid.Columns>
<DataGridTemplateColumn Header="Name"
CellTemplateSelector="{StaticResource TemplateSelector}"
CellEditingTemplateSelector="{StaticResource EditingTemplateSelector}" />
</DataGrid.Columns>
</DataGrid>
</Grid>
</Window>

WPF Custom Control: TemplateBinding to Image

I am creating a WPF custom control, a Button with an Image and Text. I have added two dependency properties to the control, ImagePath and Text, and the control template (in Themes\Generic.xaml) is a simple stack panel that arranges the image and text horizontally.
The Text property works fine. But for some reason, the sample image in my test project doesn't appear when I use TemplateBinding to the ImagePath dependency property to get its path. I have tested the image by temporarily replacing the TemplateBinding in the custom control with a path to the image, in which case it appears.
I am hoping that someone with more experience in this area can take a look and tell me why the control isn't working as expected. Thanks for your help.
My VS 2008 solution contains one project, CustomControlDemo. The project contains a custom control, TaskButton.cs, and a main window, Window1.xaml, that I use to test the control. My test image, calendar.png, is located in a Resources folder at the root level of the project, and Generic.xaml is located in a Themes folder, also at the root level of the project.
Here is the code for my custom control (from TaskButton.cs):
using System.Windows;
using System.Windows.Controls;
namespace CustomControlDemo
{
public class TaskButton : RadioButton
{
#region Fields
// Dependency property backing variables
public static readonly DependencyProperty ImagePathProperty;
public static readonly DependencyProperty TextProperty;
#endregion
#region Constructors
/// <summary>
/// Default constructor.
/// </summary>
static TaskButton()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(TaskButton), new FrameworkPropertyMetadata(typeof(TaskButton)));
// Initialize ImagePath dependency properties
ImagePathProperty = DependencyProperty.Register("ImagePath", typeof(string), typeof(TaskButton), new UIPropertyMetadata(null));
TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(TaskButton), new UIPropertyMetadata(null));
}
#endregion
#region Dependency Property Wrappers
/// <summary>
/// The ImagePath dependency property.
/// </summary>
public string ImagePath
{
get { return (string)GetValue(ImagePathProperty); }
set { SetValue(ImagePathProperty, value); }
}
/// <summary>
/// The Text dependency property.
/// </summary>
public string Text
{
get { return (string)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
#endregion
}
}
And here is the control template (from Generic.xaml):
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:CustomControlDemo">
<Style TargetType="{x:Type local:TaskButton}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:TaskButton}">
<StackPanel Height="Auto" Orientation="Horizontal">
<Image Source="{TemplateBinding ImagePath}" Width="24" Height="24" Stretch="Fill"/>
<TextBlock Text="{TemplateBinding Text}" HorizontalAlignment="Left" Foreground="{DynamicResource TaskButtonTextBrush}" FontWeight="Bold" Margin="5,0,0,0" VerticalAlignment="Center" FontSize="12" />
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
And finally, here is the Window1 markup that I am using to test the control:
<Window x:Class="CustomControlDemo.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:customControl="clr-namespace:CustomControlDemo"
Title="Window1" Height="300" Width="300">
<Grid>
<customControl:TaskButton ImagePath="Resources\calendar.png" Text="Calendar" />
</Grid>
</Window>
Any ideas why the image path isn't working? Thanks again.
I am going to leave cwap's answer as the accepted answer, because it is technically correct. However, it turns out that there is an easier way to solve this problem.
TemplateBindings aren't first-class Binding objects. They are designed to be lightweight, so they are one-way, and they lack some features of other Binding objects. Most notably, they don't support known type converters associated with a target. See MacDonald, Pro WPF in C# 2008, p. 872. That's why cwap responds correctly that I would probably need to create a type converter and reference it specifically in the control template for my custom button.
But I don't have to use a TemplateBinding to bind the control template to the ImagePath property of my custom control. I can use a plain old Binding object. Here is the revised markup for my custom control's template:
<!-- Task Button Default Control Template-->
<Style TargetType="{x:Type local:TaskButton}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:TaskButton}">
<StackPanel Height="Auto" Orientation="Horizontal">
<Image Source="{Binding Path=ImagePath, RelativeSource={RelativeSource TemplatedParent}}" Width="24" Height="24" Stretch="Fill" Margin="10,0,0,0" />
<TextBlock Text="{TemplateBinding Text}" HorizontalAlignment="Left" Foreground="{TemplateBinding Foreground}" FontWeight="Bold" Margin="5,0,10,0" VerticalAlignment="Center" FontSize="12" />
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
If you look at the ImageControl in the template, you can see the change. Note the RelativeSource property in the same object. Setting this property to ={RelativeSource TemplatedParent} is what lets me enter a relative path in my Window1 instance of the TaskButton and have it resolved correctly in the custom control.
So my recommendation for others researching this thread would be to skip the value converter and simply switch from TemplateBinding to Binding for the Image property.
Thanks also to Marco Zhou, who provided this answer to a similar question in the MSDN WPF forum.
Image doesn't take a string as a source :) You can see this in intellisense. You need to bind on an ImageSource (Or use an IValueConverter to convert the string to an ImageSource)
See this question for some tips on how to do this conversion.
Actually neither of these answers are correct.
{TemplateBinding ImagePath} is nothing more than a shortcut for {Binding Path=ImagePath, RelativeSource={RelativeSource TemplatedParent}} and as such is almost completely identical.
Also if you provide a string for ImagePath it will correctly resolve to an ImageSource although you take a hit in application performance. The real issue has to do with relative and absolute image path on the supplied ImagePath="Resources\calendar.png" in the xaml for the test. This clues the compiler to think that the supplied path is absolute because of the use of \ instead of / in defining the path.
The reason that the long form of the binding works and the shortcut doesn't is that it provides clues to the compiler that the source of the image supplied (Resources\calendar.png) is a relative path not an absolute path, therefore the image is found and the binding works. If you debug the binding you will see that the shortcut tries resolve the supplied string into an image source but can not find the file "Resources\calendar.png" If you provide a full URI to the image i.e "C:\...\Resources\calendar.png" or the corresponding blend notation of "/application;component/Resources/calendar.png" then the image will be found and the binding resolved.
This point becomes really important when you are trying to reference images from an external source instead of those compiled as resources into the final compilation.
simple way(tested)
1-make your valueConverter like this
public class objectToImageSourceConverter:IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
string packUri =value.ToString();
ImageSource Source = new ImageSourceConverter().ConvertFromString(packUri) as ImageSource;
return Source;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
2-bind your Image Source to parent's string properety (i used "tag" property)
like this xaml:
<Image HorizontalAlignment="Right" Height="Auto" Margin="0,11.75,5.5,10.75" VerticalAlignment="Stretch" Width="40.997" Source="{Binding Path=Tag, RelativeSource={RelativeSource TemplatedParent}}"/>

Resources