Триггер не срабатывает дважды и DataGridCell
Я новичок во всем этом (это мой первый пост), поэтому, пожалуйста, потерпите меня. Любые улучшения приветствуются.
Я пишу планировщик встреч с использованием.Net 4.0, Visual Studio 2010. XAML состоит из DataGrid со строками, которые разнесены на 15 минут, и четырьмя столбцами, самый левый столбец, используемый для времени. Резервные данные состоят из AppointmentRows, ObservableCollection. Каждая строка сама состоит из ObservalbeCollection of Appointmnets. Я использую DragAndDrop в качестве метода ввода.
DragAndDrop, кажется, работает правильно на уровне ячейки. Элементы могут быть сброшены в сетку данных, удалены из сетки данных и перегруппированы в пределах сетки данных. Когда встреча сбрасывается в сетке данных, первый DataTrigger устанавливает пользовательское присоединенное свойство HasAppointment в значение true, а второй триггер отвечает на это присоединенное свойство, устанавливая цвет фона DataGridCell на один из четырех цветов, в зависимости от того, пуста ли ячейка сейчас, содержит назначение кого-то, кто является новым, кто является старым, или кто требует специального управления.
Проблема № 1:
После загрузки DataGrid триггер, отвечающий за окрашивание фона ячейки, не срабатывает, если эта ячейка ранее использовалась, даже если это делает DataTrigger, отвечающий за установку присоединенного свойства HasAppointment. То есть во время загрузки и в любое время, когда в ячейке UNUSED сбрасывается встреча, все триггеры работают правильно. Однако, как только триггеры сработали в ячейке - как при первой загрузке или когда на нее было сброшено новое назначение - триггер, отвечающий за окрашивание фона, никогда не срабатывает, несмотря на то, что первый триггер правильно устанавливает свойство присоединенного свойства HasAppointment, и ячейка возвращается к его первый фоновый окрас первого назначения он содержал. Чтобы было ясно:
- DataGrid загружен. Ячейка имеет цвет по умолчанию зеленого фона и пуста.
- Пользователь сбрасывает новую встречу в ячейке. Все триггеры срабатывают, а ячейка окрашена в желтый цвет.
- Затем пользователь перетаскивает содержимое этой ячейки в другое место в сетке. Ячейка, в которой назначение БЫЛО правильно указано как пустое, HasAppointment имеет значение false, и цвет возвращается к зеленому фону по умолчанию.
- Пользователь теперь оставляет другую встречу в этой ячейке. DataTrigger, ответственный за установку присоединенного свойства HasAppointment, корректно срабатывает и устанавливает для ячейки HasAppointment значение true.
- Триггер для раскраски не срабатывает, и ячейка возвращается на желтый фон первого содержащегося в ней назначения.
Проблема № 2:
После удаления встречи в ячейке DataTrigger, ответственный за установку связанного свойства HasAppointment, запускается дважды, прежде чем разрешить запуск триггера, ответственного за раскрашивание. Он работает правильно, но почему дважды?
Проблема № 3:
Каждая ячейка может иметь или не иметь назначение. Каждая ячейка с назначением может быть одним из трех коллеров. Итак, каков наилучший способ возврата нескольких различных значений из преобразователя в DataTrigger без повторения кода для каждого значения? На данный момент я использую триггер, который хорошо работает, когда он работает.
Проблема № 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();
}
}
1 ответ
Я до сих пор не могу понять, как использовать (или если они работают) триггеры на вложенных свойствах. Я бы очень хотел увидеть как. Тем временем я обнаружил, что это решает большинство моих проблем довольно легко:
<DataGridTextColumn.ElementStyle>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Background" >
<Setter.Value>
<MultiBinding Converter="{StaticResource colorTextConverter}">
<Binding RelativeSource="{RelativeSource Mode=Self}" Path="Text" />
<Binding RelativeSource="{RelativeSource Mode=FindAncestor,AncestorType=DataGridCell}" Path="." />
</MultiBinding>
</Setter.Value>
</Setter>
</Style>
</DataGridTextColumn.ElementStyle>
</DataGridTextColumn>
Простое связывание в этом случае правильно отслеживает изменения на дисплее, которые могут пропустить триггеры.