【发布时间】:2013-10-10 06:16:59
【问题描述】:
我是新手(这是我的第一篇文章),所以请多多包涵。欢迎任何改进。
我正在使用 .Net 4.0,Visual Studio 2010 编写约会计划程序。XAML 由一个 DataGrid 组成,其中的行相隔 15 分钟,四列,最左边的一列用于时间。支持数据由 AppointmentRows 和 ObservableCollection 组成。每行本身都包含一个 ObservalbeCollection 的约会。我使用 DragAndDrop 作为我的输入法。
DragAndDrop 似乎在单元级别上正常工作。项目可以放在数据网格上,从数据网格中删除并在数据网格内重新排列。当约会放在数据网格上时,第一个 DataTrigger 将 HasAppointment 的自定义附加属性设置为 true,第二个触发器通过将 DataGridCell 背景颜色设置为四种颜色之一来响应此附加属性,具体取决于单元格现在是否为空,包含对新人、旧人或需要特殊管理的人的任命。
问题#1:
加载 DataGrid 后,如果以前使用过该单元格,则不会触发负责为单元格背景着色的触发器——即使负责设置 HasAppointment 附加属性的 DataTrigger 会触发。也就是说,在加载过程中以及任何时候 UNUSED 单元格上有约会时,所有触发器都可以正常工作。但是,一旦触发器在单元格上触发(如首次加载或在其上放置新约会时),负责背景着色的触发器永远不会触发,尽管第一个触发器正确设置了 HasAppointment 附加属性并且单元格返回到它包含的第一个约会的第一个背景颜色。明确一点:
- DataGrid 已加载。单元格的默认颜色为绿色背景并且为空。
- 用户在单元格上删除了一个新约会。所有触发器都会触发,单元格颜色为黄色。
- 然后用户将该单元格的内容拖到网格上的另一个位置。约会 WAS 正确报告为空、HasAppointment 为 false 且颜色返回默认绿色背景的单元格。
- 用户现在在该单元格上删除另一个约会。负责设置 HasAppointment 附加属性的 DataTrigger 会正确触发并将单元格 HasAppointment 设置为 true。
- 着色触发器未触发,单元格返回到其中包含的第一个约会的黄色背景。
问题#2:
在单元格上放置约会后,用于设置 HasAppointment 关联属性的 DataTrigger 在允许负责着色的触发器运行之前运行两次。它运行正常,但为什么要运行两次?
问题#3:
每个单元格可能有也可能没有约会。每个有约会的单元格可能是三种颜色中的一种。那么,在不为每个值重复代码的情况下,将多个不同值从转换器返回到 DataTrigger 的最佳方法是什么?在这个moemt,我正在使用一个触发器,当它工作时效果很好。
问题#4:
DataTrigger 是否可以对附加属性进行操作,如果可以,绑定是如何设置的?
感谢您对这些问题的任何帮助。
这是 XAML:
<Window x:Class="Chaos.Scheduler.Scheduler"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Chaos.Scheduler"
xmlns:my1="clr-namespace:UpDownCtrls;assembly=UpDownCtrls"
Title="Scheduler" Height="556" Width="1024"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
WindowStartupLocation="CenterScreen">
<!--Resources is a dictionary. so it allows assigning an x:Key value to Styles.-->
<Window.Resources>
<!--Declare converters for custom binding logic-->
<local:RescheduleConverter x:Key="rescheduleConverter" />
<local:ColorConverter x:Key="colorConverter" />
<local:AppointmentConverter x:Key="appointmentConverter" />
<local:WatchApppointmentNamesConverter x:Key="watchAppointmentNamesConverter" />
<!--Show buttons as Red when an edit has occured-->
<Style x:Key="SaveButtonStyle" TargetType="{x:Type Button}">
<Style.Triggers>
<DataTrigger Binding="{Binding HasEdits}" Value="True" >
<Setter Property="Background" Value="Red" />
</DataTrigger>
</Style.Triggers>
</Style>
<!--The XAML will create a "Style" object/instance of type "DataGridCell". Without an x:Key value, this "style" object
will be used as the parent style for all the DataGridCells. -->
<Style TargetType="{x:Type DataGridCell}" x:Key="AppointmentStyle" >
<EventSetter Event="PreviewMouseLeftButtonDown" Handler="DataGridCell_PreviewMouseLeftButtonDown" />
<EventSetter Event="MouseMove" Handler="DataGridCell_MouseMove" />
<EventSetter Event="DragEnter" Handler="DataGridCell_DragEnter" />
<EventSetter Event="Drop" Handler="DataGridCell_Drop" />
<!--Must use Right/LeftButtonUp and not Right/LeftButtonDown as these events are Direct, not tunneling or bubbled.-->
<!--This makes no sense and is probably a bug, but it works-->
<EventSetter Event="PreviewMouseRightButtonUp" Handler="DataGridCell_PreviewMouseRightButtonUp" />
<!--There is no way to replace only part of the visual tree of a control. To change the visual tree of a control you must set the
Template property of the control to its new and COMPLETE ControlTemplate. -->
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type DataGridCell}">
<!--Define the DataGridCell content. Bind the Grid control background color to the DataGridCell Style template.
The Background color will be inherited by the textblock (that is within the <Grid> by default).-->
<Grid Background="{TemplateBinding Background}">
<ContentPresenter AllowDrop="True" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
<!--Set default background color for the entire row-->
<Setter Property="Background" Value="Green"/>
<Style.Triggers>
<Trigger Property="local:Scheduler.HasAppointment" Value="true">
<Setter Property="Background">
<Setter.Value>
<MultiBinding Converter="{StaticResource colorConverter}" >
<MultiBinding.Bindings>
<!--Sends the "DataGirdCell" object to the converter-->
<Binding RelativeSource="{RelativeSource Self}"></Binding>
<!--Sends the current "DataGridRow" to the converter. The DataGridRow is the DataContext for the cell -->
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type DataGridRow}, AncestorLevel=1}" Path="." />
</MultiBinding.Bindings>
</MultiBinding>
</Setter.Value>
</Setter>
</Trigger>
<!--If the Converter returns True, then the Background will be red for the row-->
<DataTrigger Value="True">
<DataTrigger.Binding>
<!--The DataTrigger is operating on the entire row-->
<MultiBinding Converter="{StaticResource watchAppointmentNamesConverter}" ConverterParameter="0">
<!--send the DataGridCell in effect when the displayname properites change-->
<Binding RelativeSource="{RelativeSource Self}"></Binding>
<!--binding will watch the displaynames for each column. The AppointmentRow is the DataContext for all cells-->
<Binding Path="[0].displayname"></Binding>
<Binding Path="[1].displayname"></Binding>
<Binding Path="[2].displayname"></Binding>
</MultiBinding>
</DataTrigger.Binding>
<!--Any setter here will be applied to the entire row, not just the cell. There MUST HAVE be setter for the Converter to be executed-->
<Setter Property="Background" Value="Red"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid Name="MainGrid">
<!--Set AllowDrop="False" to show the scrollbars as not droppable targets-->
<DataGrid AutoGenerateColumns="False" CellStyle="{StaticResource AppointmentStyle}" ItemsSource="{Binding MySchedule.AppointmentRows}"
HorizontalAlignment="Stretch" Margin="302,47,0,37"
Name="dataGridScheduler" VerticalAlignment="Stretch" Width="688"
AllowDrop="False" SelectionUnit="Cell" SelectionMode="Single"
CanUserReorderColumns="False" CanUserAddRows="False" IsReadOnly="True" >
<!--Must have AllowDrop = True to allow dropping on the cells-->
<DataGrid.RowStyle>
<Style TargetType="{x:Type DataGridRow}">
<Setter Property="AllowDrop" Value="True" />
</Style>
</DataGrid.RowStyle>
<DataGrid.Columns>
<!--{Binding /, Path=[0].displayname} is binding to the current item of the collection, the appointment row,
with index of 0, the appointment, and shows the property of appointment displayname. The displayname must use the INotifyPropertyChanged interface-->
<DataGridTextColumn Binding="{Binding time}" Header="Time" Width="58" IsReadOnly="True" />
<DataGridTextColumn Binding="{Binding /, Path=[0].displayname}" Header="Name" Width="240*" />
<DataGridTextColumn Binding="{Binding /, Path=[1].displayname}" Header="Name" Width="240*" />
<DataGridTextColumn Binding="{Binding /, Path=[2].displayname}" Header="Name" Width="240*" />
</DataGrid.Columns>
</DataGrid>
…..........
</Window>
下面是代码:
#region Attached Property HasAppointment
// Attached properties are added to the control, not the data being displayed, and used by the XAML directly.
// Example useage in XAML: <Trigger Property="local:Scheduler.HasAppointment" Value="True">
// The attached property template is obtained by typing <propa> tab tab into the code-behind.
public static Boolean GetHasAppointment(DependencyObject obj)
{
return (Boolean)obj.GetValue(HasAppointmentProperty);
}
public static void SetHasAppointment(DependencyObject obj, Boolean value)
{
obj.SetValue(HasAppointmentProperty, value);
}
// Using a DependencyProperty as the backing store for HasAppointment. This enables animation, styling, binding, etc...
public static readonly DependencyProperty HasAppointmentProperty =
DependencyProperty.RegisterAttached("HasAppointment", typeof(Boolean), typeof(Scheduler), new UIPropertyMetadata(false));
#endregion
/* Converters apply custom logic to the XAML Bindings */
/// <summary>
/// Tests the DataGridCell for a displayname. Returns false for no displayname, true for a display name.
/// </summary>
[ValueConversion(typeof(String), typeof(Boolean))]
public class AppointmentConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
String displayname = value as String;
if (string.IsNullOrEmpty(displayname))
return false;
return true;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
/// <summary>
/// Label the reschedule shape with "Reschedule Appointment" or the patient name.
/// </summary>
[ValueConversion(typeof(Appointment), typeof(String))]
public class RescheduleConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
Appointment apt = value as Appointment;
if (apt.displayname == null) return "Reschedule Appointment";
else return apt.displayname;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
/* Color a grid cell based on appointment type and patient history */
public class ColorConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (values[1] is DataGridRow)
{
// The "cell" tells nothing about its contents, only its location in the grid, its column name and column position.
// The datacontext comes from the datagrid row.
DataGridCell cell = (DataGridCell)values[0];
if (cell.Column.DisplayIndex > 0)
{
Boolean hasappointment = Scheduler.GetHasAppointment(cell);
// the object "row" is of type AppointmentRow.
DataGridRow row = (DataGridRow)values[1];
// gets the physical row number of this row in the grid.
int rowIndex = row.GetIndex();
// SortMemberPath returns: "time", "[0].displayname", " "[1].displayname", "[2].displayname"
string columnName = cell.Column.SortMemberPath;
Appointment appointment = ((AppointmentRow)row.Item)[cell.Column.DisplayIndex - 1] as Appointment;
if (null != appointment.displayname)
{
if ((appointment.type & AppointmentType.Excision) == AppointmentType.Excision)
{
// return new SolidColorBrush(Colors.Red);
return Brushes.Red;
}
else if (null == appointment.lastvisit)
{
// return new SolidColorBrush(Colors.Yellow);
return Brushes.Yellow;
}
}
}
}
// return System.Windows.SystemColors.AppWorkspaceColor;
return new SolidColorBrush(Colors.White);
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
/// <summary>
/// Set the HasAppointment flag for all cells in the AppointmentRow.
/// For some reason, this routine will process the entire row twice on a drop. I suspect setting the HasAppointment flag forces the
/// second processeing as the "cell" object itself maybe changed.
/// [0] = DataGridCell
/// [1] = AppointmentRow[0].displayname
/// [2] = AppointmentRow[1].displayname
/// [3] = AppointmentRow[2].displayname
/// </summary>
public class WatchApppointmentNamesConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
DataGridCell cell = values[0] as DataGridCell;
if (!String.IsNullOrWhiteSpace((string)values[1]))
{
if (cell.Column.DisplayIndex == 1)
Scheduler.SetHasAppointment(cell, true);
}
else
{
if (cell.Column.DisplayIndex == 1)
Scheduler.SetHasAppointment(cell, false);
}
if (!String.IsNullOrWhiteSpace((string)values[2]))
{
if (cell.Column.DisplayIndex == 2)
Scheduler.SetHasAppointment(cell, true);
}
else
{
if (cell.Column.DisplayIndex == 2)
Scheduler.SetHasAppointment(cell, false);
}
if (!String.IsNullOrWhiteSpace((string)values[3]))
{
if (cell.Column.DisplayIndex == 3)
Scheduler.SetHasAppointment(cell, true);
}
else
{
if (cell.Column.DisplayIndex == 3)
Scheduler.SetHasAppointment(cell, false);
}
// need to return false or will force background to red.
return false;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
【问题讨论】:
-
郑重声明,如果您的问题不是文字墙,那么您将获得更好的答案。尽可能简洁,如有必要,将您的问题分成各自的主题。
-
好点。但我无法弄清楚问题出在哪里,所以我认为最好尽可能包容。调度员会不会是个问题?或者拖放机制是否有可能以某种方式干扰 Trigger,而不是 DataTrigger?
-
进一步测试表明,在后面的代码或 XAML 中设置/清除附加属性无法导致 Trigger 或 DataTrigger 再次触发。怎么办??
-
我仍然想知道如何在附加属性上正确使用触发器。由于我无法做到,我发现这是解决我大多数问题的简单方法:
标签: wpf triggers datatrigger datagridcell