CommandConverter - Valid exception or .NET Bug
I'm getting an exception in my C#/.NET application that reads:
'CommandCoverter' is unable to convert 'MyNamespace.MyDerivedFromICommandSubclass' to 'System.String'.
What I'm doing is fairly straight forward, as described by the MSDN ICommand documentation:
public class MyDerivedFromICommandSubclass : ICommand
{
// Implement interface
...
}
I have a FlowDocument with a Hyperlink on it. The Hyperlink is allowed to have a Command Property, which I set to 开发者_开发问答my derived ICommand so that when the link is clicked, my custom action gets performed.
That part works.
Here's where I get into trouble: if I select the hyperlink and right-click Copy (or press Control-C).
Instantly the .NET framework throws a System.NotSupportedException with the exception detail above. The stack trace shows:
at System.ComponentModel.TypeConverter.GetConvertToException(Object value, Type destinationType)
at System.Windows.Input.CommandConverter.ConvertTo(ITypeDescriptorContext context, CultureInfo culture, Object value, Type destinationType)
At this point I resorted to Red Gate's free .NET Reflector and looked at the source code to ConvertTo
:
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
if (destinationType == null) // We know it isn't, it's System.String
{
throw new ArgumentNullException("destinationType"); // We don't see this exception
}
if (destinationType == typeof(string)) // It is, so control passes in
{
if (value == null) // It isn't, so this condition is skipped
{
return string.Empty; // Confirmed we don't get this return value
}
RoutedCommand command = value as RoutedCommand;
if (((command != null) && (command.OwnerType != null) && IsKnownType(command.OwnerType))
{ // Is a user-defined ICommand a known type? Doubtful. This gets skipped.
return command.Name; // Confirmed we don't get this return value
}
// It has to fall through then if no return is done!
}
throw base.GetConvertToException(value, destinationType); // BOOM!
// value is my custom ICommand and destinationType is System.String
}
So the question then becomes, as all of this happens inside of .NET, am I doing something wrong, and if so, what? Or, is this a .NET bug, and if so, is there a work around?
Thanks for any help.
Intuitively this feels wrong; copying the hyperlink should copy the text, regardless of what the command does. However, you can work around the problem by implementing your own TypeConverter for your command class (How to Implement a Type Converter). Have it delegate to CommandConverter, except for CanConvertTo: return false from that method to tell the framework that your command cannot be converted to a string (or delegate CanConvertTo to CommandConverter also, then return a representative string from ConvertTo.
A fantastic description of ICommand resides in this blog entry by SkySigal, though I needed Google's Cache due to blog configuration issues at the time. Unfortunately, the end of the article where this problem is addressed is a little ambiguous in its wording about whether the ICommand should be static or non-static.
Turns out, however, there was an article on dotnet mania talking about how copying a hyperlink with a custom command will crash an application.
Seems this bug has been in .NET since 2007, at least, and that the problem is the code explicitly checking for "known commands," just as the Reflector analysis above showed.
.NET wants to serialize the command along with its parent object, and that's where the problem comes in. The article's solution involves creating a helper object, which is ignored by the serialization process, which does the same thing as the command.
<Hyperlink Command="{x:Static myns:MyCommands.CustomCommand1}" .../>
becomes
<Hyperlink myns:HyperlinkHelper.Command="{x:Static myns:MyCommands.CustomCommand1}" .../>
with some backing code inside the myns namespace's HyperlinkHelper class as a property named Command. It's clever trickery, and ought to be shamefully unnecessary.
Hats off to Eric Burke for figuring this one out.
Also banged my head on this one for quite a while. I would have added this as a comment to Walt Stoneburner's answer but seem to need more points first to do that currently.
Anyway. As the link to the original solution seem to be broken I did some more googling. I am currently running .Net Framework version 4 and this still seems to be very much unresolved (!)
There is a bug issue posted to Microsoft and together with that a spended workaround, I would think it is a similar solution as described by Walt Stoneburner. You just use another created dependency property instead of the troublesome 'Command' property on copy-paste, the rest is treated by the helper class. You can download a zip from here, press the 'Show link' to access it. Thanks Bob Bao for posting it:
http://connect.microsoft.com/VisualStudio/feedback/details/637269/copying-a-command-bound-hyperlink-in-a-flowdocument-throws-an-exception
Microsoft seem to have released a solution by now, 'HyperlinkHelper' it is called. For some awkward reason they seem to have choosen to distribute it with in some team foundation dll(?) You can find documentation for it on the link below. If you are fortunate enough to be using Team Foundation server you might even be able to use the class directly. Otherwise, I would recommend reusing the solution provided above.
http://technet.microsoft.com/en-us/subscriptions/microsoft.teamfoundation.controls.wpf.hyperlinkhelper
I just ran into this issue, in a small POC althought I got it to work in a bigger project and I managed to find why. Don't know if it can helps, but here his the context and the solution.
The problem appeared when trying to bind a command to a Button in an Xceed datagrid, which itemSource was bind on a collection exposed in my viewModel.
THE VIEW :
<UserControl x:Class="UnIfied.Module.UI.Client.Screens.Alerts.AlertsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:DataGrid="clr-namespace:Xceed.Wpf.DataGrid;assembly=Xceed.Wpf.DataGrid" xmlns:xcdg="clr-namespace:Xceed.Wpf.DataGrid.Views;assembly=Xceed.Wpf.DataGrid" xmlns:ThemePack="clr-namespace:Xceed.Wpf.DataGrid.ThemePack;assembly=Xceed.Wpf.DataGrid.ThemePack.1">
<Grid>
<DataGrid:DataGridControl Grid.Column="0"
Name="alertsBlotter"
ItemsSource="{Binding AlertsSource}"
SelectionMode="Single"
NavigationBehavior="RowOnly"
ItemScrollingBehavior="Immediate" ReadOnly="True"
AutoCreateColumns="false">
<DataGrid:DataGridControl.Columns>
<DataGrid:UnboundColumn FieldName="Acquit" Title="Acquit Alert" ReadOnly="True" ShowInColumnChooser="False">
<DataGrid:UnboundColumn.CellContentTemplate>
<DataTemplate>
<Button
DataContext="{Binding Path=DataContext, RelativeSource={RelativeSource AncestorType={x:Type DataGrid:DataRow}}}"
Content="X" Command="{Binding AcquitAlertCommand}"/>
</DataTemplate>
</DataGrid:UnboundColumn.CellContentTemplate>
</DataGrid:UnboundColumn>
<DataGrid:Column FieldName="AlertId" ReadOnly="True" Title="Alert Id" IsMainColumn="True" />
<DataGrid:Column FieldName="Time" ReadOnly="True" Title="Creation Time" />
<DataGrid:Column FieldName="AlertStatus" ReadOnly="True" Title="Status" />
<DataGrid:Column FieldName="RelatedTrade" ReadOnly="True" Title="CT Id" />
<DataGrid:Column FieldName="Status" ReadOnly="True" Title="CT Status" />
</DataGrid:DataGridControl.Columns>
<DataGrid:DataGridControl.Resources>
<Style x:Key="{x:Type DataGrid:ScrollTip}" TargetType="DataGrid:ScrollTip">
<Setter Property="HorizontalAlignment" Value="Center" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
</DataGrid:DataGridControl.Resources>
<DataGrid:DataGridControl.View>
<xcdg:TableView>
<xcdg:TableView.Theme>
<ThemePack:WMP11Theme />
</xcdg:TableView.Theme>
</xcdg:TableView>
</DataGrid:DataGridControl.View>
</DataGrid:DataGridControl>
</Grid>
THE VIEWMODEL
class AlertsViewModel : Presenter<IAlerts>
{
private readonly IAlertsService alertsService;
public AlertsViewModel(IAlerts view, IAlertsService aService)
: base(view)
{
alertsService = aService;
view.SetDataContext(this);
}
public ObservableCollection<AlertAdapter> AlertsSource
{
get { return alertsService.AlertsSource; }
}
}
THE ADAPTER (which will then be represented by a row in the datagrid). Relay command is a basic ICommand implementation.
public class AlertAdapter : BindableObject
{
private readonly RelayCommand acquitAlert;
public AlertAdapter()
{
AlertStatus = AlertStatus.Raised;
acquitAlert = new RelayCommand(ExecuteAqcuiteAlert);
}
public RelayCommand AcquitAlertCommand
{
get { return acquitAlert; }
}
private void ExecuteAqcuiteAlert(object obj)
{
AlertStatus = AlertStatus.Cleared;
}
private static readonly PropertyChangedEventArgs AlertStatusPropertyChanged = new PropertyChangedEventArgs("AlertStatus");
private AlertStatus alertStatus;
/// <summary>
/// Gets or sets the AlertStatus
/// </summary>
public AlertStatus AlertStatus
{
get { return alertStatus; }
set
{
if (AlertStatus != value)
{
alertStatus = value;
RaisePropertyChanged(AlertStatusPropertyChanged);
}
}
}
private static readonly PropertyChangedEventArgs AlertIdPropertyChanged = new PropertyChangedEventArgs("AlertId");
private Guid alertId;
/// <summary>
/// Gets or sets the AlertId
/// </summary>
public Guid AlertId
{
get { return alertId; }
set
{
if (AlertId != value)
{
alertId = value;
RaisePropertyChanged(AlertIdPropertyChanged);
}
}
}
private static readonly PropertyChangedEventArgs StatusPropertyChanged = new PropertyChangedEventArgs("Status");
private ComponentTradeStatus status;
/// <summary>
/// Gets or sets the CtStatus
/// </summary>
public ComponentTradeStatus Status
{
get { return status; }
set
{
if (Status != value)
{
status = value;
RaisePropertyChanged(StatusPropertyChanged);
}
}
}
private static readonly PropertyChangedEventArgs RelatedTradePropertyChanged = new PropertyChangedEventArgs("RelatedTrade");
private Guid relatedTrade;
/// <summary>
/// Gets or sets the RelatedTrade
/// </summary>
public Guid RelatedTrade
{
get { return relatedTrade; }
set
{
if (RelatedTrade != value)
{
relatedTrade = value;
RaisePropertyChanged(RelatedTradePropertyChanged);
}
}
}
private static readonly PropertyChangedEventArgs TimePropertyChanged = new PropertyChangedEventArgs("Time");
private DateTime time;
/// <summary>
/// Gets or sets the Time
/// </summary>
public DateTime Time
{
get { return time; }
set
{
if (Time != value)
{
time = value;
RaisePropertyChanged(TimePropertyChanged);
}
}
}
}
And here is the exception generated as soon as my app tried to create an adapter and add it to the collection
System.NotSupportedException was unhandled Message="'CommandConverter' is unable to convert 'UnIfied.Module.UI.Client.Adapters.RelayCommand' to 'System.String'." Source="System" StackTrace: at System.ComponentModel.TypeConverter.GetConvertToException(Object value, Type destinationType) at System.Windows.Input.CommandConverter.ConvertTo(ITypeDescriptorContext context, CultureInfo culture, Object value, Type destinationType) at System.ComponentModel.TypeConverter.ConvertTo(Object value, Type destinationType) at System.Windows.Controls.ContentPresenter.DefaultTemplate.DoDefaultExpansion(TextBlock textBlock, Object content, ContentPresenter container) (ETC.)
The issue was caused by the fact that my datagrid was configured to AutoCreateColumns (i.e. based on the properties of the adapter). Just switched this property to false and then all went straight again.
Hope this will help you guys !
--Bruno
The easy way to solve this issue to make Command binding with dynamic resource so that parser do not resolve while trying to convert command to string.
<Hyperlink Command="{DynamicResource NavigationCommand}">Navigate</Hyperlink>
Check details of this solution here http://ciintelligence.blogspot.com/2011/11/wpf-copying-hyperlink-with-command.html
精彩评论