Here are the requirements again.
- Add a paperclip icon to a XamDataGrid row. It is only visible when the row's item has attachments.
- When the user right-clicks, show a formatted context menu that lists the attachments.
- When the user selects a menu item, display the attachment.
To do this, the control needs some information.
- Which property contains the list of attachments
- How do we want the menu items formatted
- Which property of the attachment actually contains the attachment
So our user control needs three string dependency properties that I have called
- AttachmentsPath
- FormatString
- AttachmentPath
In this example our XamDataGrid will contain a list of Payments. Each payment may have a list of Attachments which we will access using our new user control.
Consuming the user control is very easy.
<a:AttachmentControl
AttachmentsPath="Attachments" FormatString="AttachmentName,200;Creator" AttachmentPath="Attachment"/>
Start by creating a new C# WPF UserControl project in Visual Studio called "Payments". Rename the project to AttachmentsControl and rename Control1 to AttachmentsControl.
Add references to InfragisticsWPF4.DataPresenter and InfragisticsWPF4 so your references look like this.
Find a nice paperclip icon from Images.Google.com and add it to your project. I called mine paperclip.jpg. In our XAML we will define the command used to open the attachment and an Image with an empty context menu. We define a Loaded event handler which will find the attachments and decide whether the control is visible or not. We also define an Opened event handler for the context menu so we can dynamically generate its contents. The XAML looks like this.
<UserControl x:Class="AttachmentsControl.AttachmentControl"
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:local="clr-namespace:AttachmentsControl"
mc:Ignorable="d"
DataContext="{Binding RelativeSource={RelativeSource self}}"
Loaded="UserControl_Loaded"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources>
<RoutedCommand x:Key="OpenCommand"/>
</UserControl.Resources>
<UserControl.CommandBindings>
<CommandBinding Command="{StaticResource OpenCommand}" Executed="Open_Executed"/>
</UserControl.CommandBindings>
<Image Source="paperclip.jpg" Height="16">
<Image.ContextMenu>
<ContextMenu Opened="ContextMenu_Opened"/>
</Image.ContextMenu>
</Image>
</UserControl>
public static readonly DependencyProperty
AttachmentsPathProperty = DependencyProperty.Register("AttachmentsPath", typeof(String),
typeof(AttachmentControl), new PropertyMetadata("Attachments"));
public static readonly DependencyProperty
FormatStringProperty = DependencyProperty.Register("FormatString", typeof(String), typeof(AttachmentControl), new PropertyMetadata(""));
public static readonly DependencyProperty
AttachmentPathProperty = DependencyProperty.Register("AttachmentPath", typeof(String), typeof(AttachmentControl), new PropertyMetadata("Attachment"));
public static string GetAttachmentsPath(DependencyObject
obj)
{
return (string)obj.GetValue(AttachmentsPathProperty);
}
public static void SetAttachmentsPath(DependencyObject
obj, string value)
{
obj.SetValue(AttachmentsPathProperty, value);
}
public static string GetFormatString(DependencyObject
obj)
{
return (String)obj.GetValue(FormatStringProperty);
}
public static void SetFormatString(DependencyObject
obj, string value)
{
obj.SetValue(FormatStringProperty,
value);
}
public static String
GetAttachmentPath(DependencyObject obj)
{
return (String)obj.GetValue(AttachmentPathProperty);
}
public static void SetAttachmentPath(DependencyObject
obj, String value)
{
obj.SetValue(AttachmentPathProperty, value);
}
private class cFormat:IDisposable
{
public String MemberName;
public Double Width;
public void
Dispose()
{
}
}
IEnumerable<object>
Attachments;
Let's look at the Loaded event handler. It has to find the DataRecord's attachments and decide if we are visible. We take the sender (which is a user control) and walk up the visual tree until we find an ancestor that has a DataContext that is an Infragistics DataRecord.
Using the AttachmentsPath property we get the attachments collection and store it in our Attachments field.
private void
UserControl_Loaded(object sender,
RoutedEventArgs e)
{
string AttachmentsPath = (String)GetValue(AttachmentsPathProperty);
object di;
// Walk up the
visual tree until I find something with a DataContext that is a DataRecord
DependencyObject p = sender as DependencyObject;
while (!((p as
FrameworkElement).DataContext is DataRecord))
p =
VisualTreeHelper.GetParent(p);
di = ((p as FrameworkElement).DataContext as DataRecord).DataItem;
if (di != null)
Attachments = di.GetType().GetProperty(AttachmentsPath).GetValue(di) as IEnumerable<object>;
// am I
visible?
this.Visibility = (Attachments == null || Attachments.Count() == 0) ? Visibility.Hidden :
Visibility.Visible;
}
The meat of the code is in the ContextMenu opened event handler. If we don't have attachments or a FormatString we hide the menu, otherwise we parse the FormatString and populate the Format collection.
Then, for each attachment we create a new menu item containing a StackPanel and, for each Format, we add a TextBlock to the StackPanel. We use reflection to populate the TextBlocks. We also add the OpenCommand to each menu item and pass the attachment as the parameter. In real life, this would be a Byte(). It is important to explicitly assign the MenuItem.CommandTarget otherwise the menu item would not be selectable.
private void
ContextMenu_Opened(object sender,
RoutedEventArgs e)
{
ContextMenu cm =
(ContextMenu)sender;
String FormatString =
(String)GetValue(FormatStringProperty);
List<cFormat> Formats = new List<cFormat>();
RoutedCommand oa = this.FindResource("OpenCommand") as RoutedCommand;
string AttachmentPath = (String)GetValue(AttachmentPathProperty);
if (Attachments == null || Attachments.Count() == 0 || FormatString == "")
{
cm.Visibility =
Visibility.Collapsed;
return;
}
// Expecting
property name and optional width. You can jazz this up if you want
// eg
TransmittalNumber,60;DocumentNumber,60;AttachmentName,200;Creator
foreach (String format in FormatString.Split(';'))
{
List<String> FormatParts
= format.Split(',').ToList();
using (cFormat oFormat = new cFormat())
{
oFormat.MemberName =
FormatParts[0];
if (FormatParts.Count > 1)
Double.TryParse(FormatParts[1], out oFormat.Width);
else
oFormat.Width = double.NaN;
Formats.Add(oFormat);
}
}
cm.Items.Clear();
foreach (object o in Attachments)
{
Type t = o.GetType();
MenuItem mi = new MenuItem();
StackPanel sp = new StackPanel() { Orientation =
Orientation.Horizontal };
foreach (cFormat oFormat in Formats)
{
TextBlock tb = new TextBlock();
PropertyInfo pi =
t.GetProperty(oFormat.MemberName);
if (pi == null)
tb.Text =
oFormat.MemberName;
else
tb.Text = (string)pi.GetValue(o);
tb.Width = oFormat.Width;
tb.Margin = new Thickness(5, 0, 0, 0);
tb.TextTrimming =
TextTrimming.CharacterEllipsis;
sp.Children.Add(tb);
}
mi.Header = sp;
mi.Command = oa;
mi.CommandParameter =
t.GetProperty(AttachmentPath).GetValue(o) as string;
mi.CommandTarget =
cm.PlacementTarget;
cm.Items.Add(mi);
}
}
The last method we need is the OpenCommand_Executed. Normally you would use Process.Start or something to display the attachment. Ours are simple strings so I use a MessageBox.
private void
Open_Executed(object sender,
ExecutedRoutedEventArgs e)
{
// Replace this
with a call to Process.Start
MessageBox.Show(e.Parameter as string,"Attachment");
}
Add a WPF App called Payments to the solution. Make sure it's the Startup project. It will need references to InfragisticsWPF4.DataPresenter, InfragisticsWPF4.Editors, and InfragisticsWPF4 like this.
The XAML is a "simple" XAMDataGrid (is there really such a thing?). The user control goes into an unbound template field.
<Window x:Class="Payments.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:igDP="http://infragistics.com/DataPresenter"
xmlns:a="clr-namespace:AttachmentsControl;assembly=AttachmentsControl"
xmlns:local="clr-namespace:Payments"
mc:Ignorable="d"
DataContext="{Binding RelativeSource={RelativeSource self}}"
Title="Payments" Height="450" Width="800">
<Grid>
<igDP:XamDataGrid DataSource="{Binding Transmittals}">
<igDP:XamDataGrid.FieldLayoutSettings>
<igDP:FieldLayoutSettings AutoGenerateFields="False"/>
</igDP:XamDataGrid.FieldLayoutSettings>
<igDP:XamDataGrid.FieldSettings>
<igDP:FieldSettings AllowSummaries="False" AllowEdit="False"/>
</igDP:XamDataGrid.FieldSettings>
<igDP:XamDataGrid.FieldLayouts>
<igDP:FieldLayout Description="Transmittals" Key="Transmittals">
<igDP:FieldLayout.Fields>
<igDP:TemplateField Label="" BindingType="Unbound" Width="20">
<igDP:TemplateField.DisplayTemplate>
<DataTemplate>
<a:AttachmentControl AttachmentsPath="Attachments" FormatString="AttachmentName,200;Creator" AttachmentPath="Attachment"/>
</DataTemplate>
</igDP:TemplateField.DisplayTemplate>
</igDP:TemplateField>
<igDP:TextField Label="Payment
Number" Name="PaymentNumber"/>
<igDP:TextField Label="Description" Name="Description" Width="auto"/>
</igDP:FieldLayout.Fields>
</igDP:FieldLayout>
</igDP:XamDataGrid.FieldLayouts>
</igDP:XamDataGrid>
</Grid>
</Window>
using System.Collections.Generic;
using System.Windows;namespace Payments
{
public partial class MainWindow : Window
{
public class cPayment
{
public string
PaymentNumber { get; set; }
public string
Description { get; set; }
public List<cAttachment> Attachments { get; set; }
}
public class cAttachment
{
public string
AttachmentName { get; set; }
public string
Attachment { get; set; }
public string Creator
{ get; set; }
}
public List<cPayment> Transmittals
{
get
{
return new
List<cPayment>()
{
new cPayment()
{
PaymentNumber ="123456",
Description = "Payment to a vendor",
Attachments=new List<cAttachment>()
{
new cAttachment() {AttachmentName="County guidelines", Attachment="These are the detailed County Guidelines for doing
things", Creator="rsnipper"},
new cAttachment() {AttachmentName="Vendor contract", Attachment="Here is the vendor contract covering the things we are
doing", Creator="abaglady"},
new cAttachment() {AttachmentName="A ridiculously long attachment name that goes on
forever", Attachment="Honestly, some of these attachments are way too
long.", Creator="rsmith"}
}
},
new cPayment()
{
PaymentNumber = "123478",
Description = "Payment to a different vendor",
Attachments=new List<cAttachment>()
{
new cAttachment() {AttachmentName="Other Vendor contract",
Attachment="This is the contract for a different vendor that
covers the kinds of things we're doing", Creator="sblotty"}
}
},
new cPayment()
{
PaymentNumber="876543",
Description="Payment with no attachments"
}
};
}
}
public MainWindow()
{
InitializeComponent();
}
}
}