Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add datagrid combo box column #18105

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
231 changes: 231 additions & 0 deletions src/Avalonia.Controls.DataGrid/DataGridComboBoxColumn.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
using System;
using System.Collections;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml.MarkupExtensions;
using Avalonia.Metadata;
using Avalonia.Styling;

namespace Avalonia.Controls;

public class DataGridComboBoxColumn : DataGridBoundColumn
{
/// <summary>
/// Defines the <see cref="DisplayMemberBinding" /> property
/// </summary>
public static readonly StyledProperty<IBinding> DisplayMemberBindingProperty =
ItemsControl.DisplayMemberBindingProperty.AddOwner<DataGridComboBoxColumn>();

/// <summary>
/// Defines the <see cref="ItemsSource"/> property.
/// </summary>
public static readonly StyledProperty<IEnumerable> ItemsSourceProperty =
ItemsControl.ItemsSourceProperty.AddOwner<DataGridComboBoxColumn>();

/// <summary>
/// Defines the <see cref="ItemTemplate"/> property.
/// </summary>
public static readonly StyledProperty<IDataTemplate> ItemTemplateProperty =
ItemsControl.ItemTemplateProperty.AddOwner<DataGridComboBoxColumn>();

/// <summary>
/// Defines the <see cref="SelectedValue"/> property
/// </summary>
public static readonly StyledProperty<IBinding> SelectedValueProperty =
//we cant use SelectingItemsControl.SelectedValue here because we need the binding to check for readonly status
AvaloniaProperty.Register<DataGridComboBoxColumn, IBinding>(nameof(SelectedValue));

/// <summary>
/// Defines the <see cref="SelectedValueBinding"/> property
/// </summary>
public static readonly StyledProperty<IBinding> SelectedValueBindingProperty =
SelectingItemsControl.SelectedValueBindingProperty.AddOwner<DataGridComboBoxColumn>();

/// <summary>
/// Defines the <see cref="SelectionBoxItemTemplate"/> property.
/// </summary>
public static readonly StyledProperty<IDataTemplate> SelectionBoxItemTemplateProperty =
ComboBox.SelectionBoxItemTemplateProperty.AddOwner<DataGridComboBoxColumn>();

/// <inheritdoc cref="ItemsControl.DisplayMemberBinding"/>
[AssignBinding, InheritDataTypeFromItems(nameof(ItemsSource))]
public IBinding DisplayMemberBinding
{
get => GetValue(DisplayMemberBindingProperty);
set => SetValue(DisplayMemberBindingProperty, value);
}


/// <inheritdoc cref="ItemsControl.ItemsSource"/>
public IEnumerable ItemsSource
{
get => GetValue(ItemsSourceProperty);
set => SetValue(ItemsSourceProperty, value);
}

/// <inheritdoc cref="ItemsControl.ItemTemplate"/>
[InheritDataTypeFromItems(nameof(DataGrid.ItemsSource), AncestorType = typeof(DataGrid))]
public IDataTemplate ItemTemplate
{
get => GetValue(ItemTemplateProperty);
set => SetValue(ItemTemplateProperty, value);
}

/// <summary>
/// The binding used to get the value of the selected item
/// </summary>
[AssignBinding, InheritDataTypeFromItems(nameof(DataGrid.ItemsSource), AncestorType = typeof(DataGrid))]
public IBinding SelectedValue
{
get => GetValue(SelectedValueProperty);
set => SetValue(SelectedValueProperty, value);
}

/// <summary>
/// A binding used to get the value of an item selected in the combobox
/// </summary>
[AssignBinding, InheritDataTypeFromItems(nameof(ItemsSource), AncestorType = typeof(DataGridComboBoxColumn))]
public IBinding SelectedValueBinding
{
get => GetValue(SelectedValueBindingProperty);
set => SetValue(SelectedValueBindingProperty, value);
}

/// <summary>
/// Gets or sets the data template used to display the item in the combo box (not the dropdown)
/// </summary>
[InheritDataTypeFromItems(nameof(ItemsSource))]
public IDataTemplate SelectionBoxItemTemplate
{
get => GetValue(SelectionBoxItemTemplateProperty);
set => SetValue(SelectionBoxItemTemplateProperty, value);
}

[AssignBinding, InheritDataTypeFromItems(nameof(DataGrid.ItemsSource), AncestorType = typeof(DataGrid))]
public virtual IBinding SelectedItemBinding
{
get => Binding;
set => Binding = value;
}


private readonly Lazy<ControlTheme> _cellComboBoxTheme;

public DataGridComboBoxColumn()
{
BindingTarget = SelectingItemsControl.SelectedItemProperty;

_cellComboBoxTheme = new Lazy<ControlTheme>(() =>
OwningGrid.TryFindResource("DataGridCellComboBoxTheme", out var theme) ? (ControlTheme)theme : null);
}

protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);

if (change.Property == DisplayMemberBindingProperty
|| change.Property == ItemsSourceProperty
|| change.Property == ItemTemplateProperty
|| change.Property == SelectedValueProperty
|| change.Property == SelectedValueBindingProperty
|| change.Property == SelectionBoxItemTemplateProperty)
{
NotifyPropertyChanged(change.Property.Name);
}

//if using the SelectedValue binding then the combobox needs to be bound using the selected value
//otherwise use the default SelectedItem
if (change.Property == SelectedValueProperty)
BindingTarget = change.NewValue == null
? SelectingItemsControl.SelectedItemProperty
: SelectingItemsControl.SelectedValueProperty;
}

/// <summary>
/// Gets a <see cref="T:Avalonia.Controls.ComboBox" /> control that is bound to the column's <see cref="SelectedItemBinding"/> property value.
/// </summary>
/// <param name="cell">The cell that will contain the generated element.</param>
/// <param name="dataItem">The data item represented by the row that contains the intended cell.</param>
/// <returns>A new <see cref="T:Avalonia.Controls.ComboBox" /> control that is bound to the column's <see cref="SelectedItemBinding"/> property value.</returns>
protected override Control GenerateEditingElementDirect(DataGridCell cell, object dataItem)
{
ComboBox comboBox = new ComboBox
{
Name = "CellComboBox"
};

if (_cellComboBoxTheme.Value is { } theme)
comboBox.Theme = theme;

SyncProperties(comboBox);

return comboBox;
}

protected override Control GenerateElement(DataGridCell cell, object dataItem)
{
ComboBox comboBox = new ComboBox
{
Name = "DisplayValueComboBox",
IsHitTestVisible = false
};

if (_cellComboBoxTheme.Value is { } theme)
comboBox.Theme = theme;

SyncProperties(comboBox);

//if (Binding != null && dataItem != DataGridCollectionView.NewItemPlaceholder)
comboBox.Bind(BindingTarget, Binding);

return comboBox;
}

protected override object PrepareCellForEdit(Control editingElement, RoutedEventArgs editingEventArgs)
{
if (editingElement is ComboBox comboBox)
{
comboBox.IsDropDownOpen = true;
if (BindingTarget == SelectingItemsControl.SelectedValueProperty)
return comboBox.SelectedValue;

return comboBox.SelectedItem;
}
return null;
}

private void SyncProperties(ComboBox comboBox)
{
DataGridHelper.SyncColumnProperty(this, comboBox, DisplayMemberBindingProperty);
DataGridHelper.SyncColumnProperty(this, comboBox, ItemsSourceProperty);
DataGridHelper.SyncColumnProperty(this, comboBox, ItemTemplateProperty);
DataGridHelper.SyncColumnProperty(this, comboBox, SelectedValueBindingProperty);
DataGridHelper.SyncColumnProperty(this, comboBox, SelectionBoxItemTemplateProperty);

//if binding using SelectedItem then the DataGridBoundColumn handles that, otherwise we need to
if (BindingTarget == SelectingItemsControl.SelectedValueProperty)
comboBox.Bind(SelectingItemsControl.SelectedValueProperty, SelectedValue);
}

public override bool IsReadOnly
{
get
{
if (OwningGrid == null)
return base.IsReadOnly;
if (OwningGrid.IsReadOnly)
return true;

var valueBinding = Binding ?? SelectedValue;
string path = (valueBinding as Binding)?.Path
?? (valueBinding as CompiledBindingExtension)?.Path.ToString();
return OwningGrid.DataConnection.GetPropertyIsReadOnly(path, out _);
}
set
{
base.IsReadOnly = value;
}
}
}
76 changes: 52 additions & 24 deletions src/Avalonia.Controls.DataGrid/DataGridDataConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -377,37 +377,24 @@ public object GetDataItem(int index)
return null;
}

/// <summary>
/// Check if a property is readonly only (has a setter, isn't <see cref="EditableAttribute.AllowEdit"/>
/// and that the property is an editable basic type)
/// </summary>
/// <param name="propertyName">The name or a path to the property</param>
/// <remarks>
/// To check if the property can be written too and not check it's type use <see cref="GetPropertyIsReadOnly(string, out Type)"/>
/// </remarks>
public bool GetPropertyIsReadOnly(string propertyName)
{
if (DataType != null)
{
if (!String.IsNullOrEmpty(propertyName))
{
Type propertyType = DataType;
PropertyInfo propertyInfo = null;
List<string> propertyNames = TypeHelper.SplitPropertyPath(propertyName);
for (int i = 0; i < propertyNames.Count; i++)
{
propertyInfo = propertyType.GetPropertyOrIndexer(propertyNames[i], out _);
if (propertyInfo == null || propertyType.GetIsReadOnly() || propertyInfo.GetIsReadOnly())
{
// Either the data type is read-only, the property doesn't exist, or it does exist but is read-only
return true;
}
if (GetPropertyIsReadOnly(propertyName, out Type propertyType))
return true;

// Check if EditableAttribute is defined on the property and if it indicates uneditable
var attributes = propertyInfo.GetCustomAttributes(typeof(EditableAttribute), true);
if (attributes != null && attributes.Length > 0)
{
var editableAttribute = (EditableAttribute)attributes[0];
if (!editableAttribute.AllowEdit)
{
return true;
}
}
propertyType = propertyInfo.PropertyType.GetNonNullableType();
}
return propertyInfo == null || !propertyInfo.CanWrite || !AllowEdit || !CanEdit(propertyType);
return !AllowEdit || !CanEdit(propertyType);
}
else if (DataType.GetIsReadOnly())
{
Expand All @@ -417,6 +404,47 @@ public bool GetPropertyIsReadOnly(string propertyName)
return !AllowEdit;
}

/// <summary>
/// Check if a property is readonly based on if it has a setter
/// </summary>
/// <param name="propertyName">The name or a path to the property</param>
/// <param name="propertyType">The property type found via the path</param>
public bool GetPropertyIsReadOnly(string propertyName, out Type propertyType)
{
propertyType = null;
if (DataType == null)
return true;

if (string.IsNullOrEmpty(propertyName))
return true;

propertyType = DataType;
PropertyInfo propertyInfo = null;
List<string> propertyNames = TypeHelper.SplitPropertyPath(propertyName);
for (int i = 0; i < propertyNames.Count; i++)
{
propertyInfo = propertyType.GetPropertyOrIndexer(propertyNames[i], out _);
if (propertyInfo == null || propertyType.GetIsReadOnly() || propertyInfo.GetIsReadOnly())
{
// Either the data type is read-only, the property doesn't exist, or it does exist but is read-only
return true;
}

// Check if EditableAttribute is defined on the property and if it indicates uneditable
var attributes = propertyInfo.GetCustomAttributes(typeof(EditableAttribute), true);
if (attributes != null && attributes.Length > 0)
{
var editableAttribute = (EditableAttribute)attributes[0];
if (!editableAttribute.AllowEdit)
{
return true;
}
}
propertyType = propertyInfo.PropertyType.GetNonNullableType();
}
return propertyInfo == null || !propertyInfo.CanWrite;
}

public int IndexOf(object dataItem)
{
if (DataSource is DataGridCollectionView cv)
Expand Down
3 changes: 3 additions & 0 deletions src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@
<Setter Property="Theme" Value="{StaticResource TooltipDataValidationErrors}" />
</Style>
</ControlTheme>
<ControlTheme x:Key="DataGridCellComboBoxTheme" TargetType="ComboBox" BasedOn="{StaticResource {x:Type ComboBox}}">
<Setter Property="HorizontalAlignment" Value="Stretch"/>
</ControlTheme>

<ControlTheme x:Key="{x:Type DataGridCell}" TargetType="DataGridCell">
<Setter Property="Background" Value="{DynamicResource DataGridCellBackgroundBrush}" />
Expand Down
3 changes: 3 additions & 0 deletions src/Avalonia.Controls.DataGrid/Themes/Simple.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="Background" Value="Transparent" />
</ControlTheme>
<ControlTheme x:Key="DataGridCellComboBoxTheme" TargetType="ComboBox" BasedOn="{StaticResource {x:Type ComboBox}}">
<Setter Property="HorizontalAlignment" Value="Stretch"/>
</ControlTheme>

<ControlTheme x:Key="{x:Type DataGridCell}"
TargetType="DataGridCell">
Expand Down