Avalonia数据绑定:共享模型类示例
本示例提供了用于 Avalonia 数据绑定的共享模型类,包括 Category、Product 以及数据访问类 StoreDb1 和 StoreDbDataSet。此外,还包含了用于加载数据的 store.xml 和 store.xsd 文件。
Category.cs
using CommunityToolkit.Mvvm.ComponentModel;
using System.Collections.ObjectModel;
namespace AvaloniaUI.Demos.Book._19.StoreDatabase
{
public partial class Category : ObservableObject
{
[ObservableProperty]
private string? categoryName;
[ObservableProperty]
private int categoryId;
[ObservableProperty]
private ObservableCollection<Product> products = new();
public Category(string categoryName, ObservableCollection<Product> products)
{
CategoryName = categoryName;
Products = products;
}
public Category(string categoryName, int categoryId)
{
CategoryName = categoryName;
CategoryId = categoryId;
}
}
}
Product.cs
包含 Product 类以及两个用于数据转换的辅助类:PriceToBackgroundConverter 和 ImagePathToBitmapConverter。
using Avalonia.Data.Converters;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using CommunityToolkit.Mvvm.ComponentModel;
using System;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
namespace AvaloniaUI.Demos.Book._19.StoreDatabase
{
public class PriceToBackgroundConverter : IValueConverter
{
public IBrush? HighlightBrush { get; set; }
public decimal MinPrice { get; set; } = 0;
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is Product product)
{
return product.UnitCost >= MinPrice ? HighlightBrush : Brushes.Transparent;
}
return Brushes.Transparent;
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotSupportedException("This converter does not support conversion back.");
}
}
public class ImagePathToBitmapConverter : IValueConverter
{
private const string ResourcePrefix = "avares://AvaloniaUI/Resources/Images/";
public static readonly ImagePathToBitmapConverter Instance = new();
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is not string fileName || string.IsNullOrWhiteSpace(fileName))
return null;
try
{
var uri = new Uri(ResourcePrefix + fileName);
using (var stream = AssetLoader.Open(uri))
{
return new Bitmap(stream);
}
}
catch (Exception)
{
// Log exception if needed
return null;
}
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
=> throw new NotSupportedException("This converter does not support conversion back.");
}
public partial class Product : ObservableValidator
{
[ObservableProperty]
[NotifyDataErrorInfo]
[Required(ErrorMessage = "Model number is required.")]
private string? modelNumber;
[ObservableProperty]
[NotifyDataErrorInfo]
[Required(ErrorMessage = "Model name is required.")]
[MinLength(3, ErrorMessage = "Model name must be at least 3 characters.")]
private string? modelName;
[ObservableProperty]
[NotifyDataErrorInfo]
[Range(0.01, 999.99, ErrorMessage = "Unit cost must be between 0.01 and 999.99.")]
private decimal unitCost;
[ObservableProperty]
private string? description;
[ObservableProperty]
private string? categoryName;
[ObservableProperty]
private int categoryId;
[ObservableProperty]
private string? productImagePath;
[ObservableProperty]
private DateTime dateAdded = DateTime.Today;
public bool IsTravel => CategoryName == "Travel";
public Product(string modelNumber, string modelName, decimal unitCost, string description)
{
ModelNumber = modelNumber;
ModelName = modelName;
UnitCost = unitCost;
Description = description;
}
public Product(string modelNumber, string modelName, decimal unitCost, string description, string productImagePath)
: this(modelNumber, modelName, unitCost, description)
{
ProductImagePath = productImagePath;
}
public Product(string modelNumber, string modelName, decimal unitCost, string description, int categoryId, string categoryName, string productImagePath)
: this(modelNumber, modelName, unitCost, description)
{
CategoryId = categoryId;
CategoryName = categoryName;
ProductImagePath = productImagePath;
}
public override string ToString()
{
return $"{ModelName} ({ModelNumber})";
}
}
}
StoreDb1.cs
提供了从 DataSet 加载产品和分类数据的方法。
using System;
using System.Collections.ObjectModel;
using System.Data;
using System.Linq;
namespace AvaloniaUI.Demos.Book._19.StoreDatabase
{
public class StoreDb1
{
public Product? GetProductById(int id)
{
DataSet? ds = StoreDbDataSet.LoadData();
DataTable? productsTable = ds?.Tables["Products"];
if (productsTable == null) return null;
DataRow? row = productsTable.AsEnumerable()
.FirstOrDefault(r => r.Field<int>("ProductID") == id);
return row != null ? CreateProductFromRow(row) : null;
}
public ObservableCollection<Product> GetAllProducts()
{
DataSet? ds = StoreDbDataSet.LoadData();
DataTable? productsTable = ds?.Tables["Products"];
var products = new ObservableCollection<Product>();
if (productsTable == null) return products;
foreach (DataRow row in productsTable.Rows)
{
products.Add(CreateProductFromRow(row));
}
return products;
}
public ObservableCollection<Category> GetCategoriesWithProducts()
{
DataSet? ds = StoreDbDataSet.LoadData();
DataTable? categoriesTable = ds?.Tables["Categories"];
DataTable? productsTable = ds?.Tables["Products"];
var categories = new ObservableCollection<Category>();
if (categoriesTable == null || productsTable == null) return categories;
DataRelation? relation = ds?.Relations["CategoryProduct"]; // Assuming the relation name is 'CategoryProduct'
foreach (DataRow categoryRow in categoriesTable.Rows)
{
var categoryProducts = new ObservableCollection<Product>();
if (relation != null)
{
foreach (DataRow productRow in categoryRow.GetChildRows(relation))
{
categoryProducts.Add(CreateProductFromRow(productRow));
}
}
string? name = categoryRow["CategoryName"]?.ToString();
categories.Add(new Category(name ?? "", categoryProducts));
}
return categories;
}
public ObservableCollection<Product> GetProductsAboveCost(decimal minCost)
{
var allProducts = GetAllProducts();
var filteredProducts = allProducts.Where(p => p.UnitCost >= minCost).ToList();
return new ObservableCollection<Product>(filteredProducts);
}
public ObservableCollection<Category> GetCategories()
{
DataSet? ds = StoreDbDataSet.LoadData();
DataTable? table = ds?.Tables["Categories"];
var categories = new ObservableCollection<Category>();
if (table == null) return categories;
foreach (DataRow row in table.Rows)
{
string? name = row["CategoryName"]?.ToString();
int id = row.Field<int>("CategoryID");
categories.Add(new Category(name ?? "", id));
}
return categories;
}
private Product CreateProductFromRow(DataRow row)
{
return new Product(
row["ModelNumber"]?.ToString() ?? "",
row["ModelName"]?.ToString() ?? "",
row.Field<decimal>("UnitCost"),
row["Description"]?.ToString() ?? "",
row.Field<int>("CategoryID"),
row["CategoryName"]?.ToString() ?? "",
row["ProductImage"]?.ToString() ?? ""
);
}
}
}
StoreDbDataSet.cs
负责加载 store.xml 和 store.xsd 文件,并返回一个 DataSet 对象。
using Avalonia.Platform;
using System;
using System.Data;
namespace AvaloniaUI.Demos.Book._19.StoreDatabase
{
public static class StoreDbDataSet
{
private static DataSet? _dataSet;
public static DataSet? LoadData()
{
if (_dataSet != null)
{
return _dataSet;
}
try
{
_dataSet = new DataSet();
var xsdUri = new Uri("avares://AvaloniaUI/Resources/Datas/store.xsd");
var xmlUri = new Uri("avares://AvaloniaUI/Resources/Datas/store.xml");
using (var xsdStream = AssetLoader.Open(xsdUri))
using (var xmlStream = AssetLoader.Open(xmlUri))
{
_dataSet.ReadXmlSchema(xsdStream);
_dataSet.ReadXml(xmlStream);
}
if (_dataSet.Tables.Count == 0)
{
throw new InvalidOperationException("Failed to load any tables from the data source.");
}
return _dataSet;
}
catch (Exception ex)
{
Console.WriteLine($"Error loading data: {ex.Message}");
_dataSet = null; // Reset on error
return null;
}
}
}
}
store.xml
包含产品和分类的示例数据。
<?xml version="1.0" standalone="yes"?>
<NewDataSet>
<Products>
<ProductID>355</ProductID>
<CategoryID>16</CategoryID>
<ModelNumber>RU007</ModelNumber>
<ModelName>Rain Racer 2000</ModelName>
<ProductImage>360.gif</ProductImage>
<UnitCost>1499.9900</UnitCost>
<Description>Looks like an ordinary bumbershoot, but don't be fooled! ...</Description>
<CategoryID1>16</CategoryID1>
<CategoryName>Travel</CategoryName>
</Products>
<Products>
<ProductID>356</ProductID>
<CategoryID>20</CategoryID>
<ModelNumber>STKY1</ModelNumber>
<ModelName>Edible Tape</ModelName>
<ProductImage>image.gif</ProductImage>
<UnitCost>3.9900</UnitCost>
<Description>The latest in personal survival gear, the STKY1 looks like a roll of ordinary office tape...</Description>
<CategoryID1>20</CategoryID1>
<CategoryName>General</CategoryName>
</Products>
<!-- ... more product entries ... -->
<Categories>
<CategoryID>14</CategoryID>
<CategoryName>Communications</CategoryName>
</Categories>
<Categories>
<CategoryID>15</CategoryID>
<CategoryName>Deception</CategoryName>
</Categories>
<!-- ... more category entries ... -->
</NewDataSet>
store.xsd
定义了 store.xml 的数据结构和约束。
<?xml version="1.0" standalone="yes"?>
<xs:schema id="NewDataSet" xmlns="" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xs:element name="NewDataSet" msdata:IsDataSet="true" msdata:UseCurrentLocale="true">
<xs:complexType>
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="Products">
<xs:complexType>
<xs:sequence>
<xs:element name="ProductID" msdata:ReadOnly="true" msdata:AutoIncrement="true" type="xs:int" />
<xs:element name="CategoryID" type="xs:int" />
<xs:element name="ModelNumber" minOccurs="0">
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:maxLength value="50" />
</xs:restriction>
</xs:simpleType>
</xs:element>
<!-- ... other product elements ... -->
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="Categories">
<xs:complexType>
<xs:sequence>
<xs:element name="CategoryID" msdata:ReadOnly="true" msdata:AutoIncrement="true" type="xs:int" />
<xs:element name="CategoryName" minOccurs="0">
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:maxLength value="50" />
</xs:restriction>
</xs:simpleType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:choice>
</xs:complexType>
<xs:unique name="Constraint1" msdata:PrimaryKey="true">
<xs:selector xpath=".//Products" />
<xs:field xpath="ProductID" />
<xs:field xpath="CategoryID1" />
</xs:unique>
<xs:unique name="Categories_Constraint1" msdata:ConstraintName="Constraint1" msdata:PrimaryKey="true">
<xs:selector xpath=".//Categories" />
<xs:field xpath="CategoryID" />
</xs:unique>
<xs:keyref name="CategoryProduct" refer="Categories_Constraint1">
<xs:selector xpath=".//Products" />
<xs:field xpath="CategoryID" />
</xs:keyref>
</xs:element>
</xs:schema>