У меня проблемы с пониманием того, как работает привязка в .NET MAUI. Моя цель состоит в том, чтобы иметь два способа отображения оценки внутри CollectionView: метка с числом и графическое представление. Привязка партитуры к метке работает нормально, а вот привязка к Drawable — нет. Если я пишу число вместо использования привязки, оно передается просто отлично.
Что я делаю не так?
ПроцедурыPage.xaml
<?xml version = "1.0" encoding = "utf-8" ?>
<ContentPage xmlns = "http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x = "http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:model = "clr-namespace:MediSkillApp.Model"
xmlns:drawables = "clr-namespace:MediSkillApp.Drawables"
xmlns:viewmodel = "clr-namespace:MediSkillApp.ViewModel"
x:Class = "MediSkillApp.View.ProceduresPage"
x:DataType = "viewmodel:ProceduresViewModel"
Title = "Alle mine indgreb">
<Grid ColumnDefinitions = "*"
ColumnSpacing = "5"
RowSpacing = "0">
<CollectionView
BackgroundColor = "Transparent"
ItemsSource = "{Binding Procedures}"
RemainingItemsThresholdReachedCommand = "{Binding GetProceduresCommand}"
RemainingItemsThreshold = "5">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType = "model:Procedure">
<VerticalStackLayout>
<Frame Margin = "5">
<Grid ColumnDefinitions = "64,*, 64">
<Image
Grid.Column = "0"
Source = "{Binding Icon}"
HeightRequest = "48"
WidthRequest = "48"
HorizontalOptions = "Start"/>
<VerticalStackLayout
Grid.Column = "1">
<Label Text = "{Binding ProcedureTypeString}"
Style = "{StaticResource Heading}"/>
<Label Text = "{Binding OpRoleString}"
Style = "{StaticResource NormalLabel}"/>
<Label Text = "{Binding Date, StringFormat='{0:dd/MM-yyyy}'}"
Style = "{StaticResource NormalLabel}"/>
</VerticalStackLayout>
<VerticalStackLayout
Grid.Column = "2"
IsVisible = "{Binding IsScored}">
<!-- this binding works -->
<Label Text = "{Binding AvgScore, StringFormat='{0:F2}'}"
HorizontalOptions = "Center"/>
<Image Source = "scoremeter.png"/>
<GraphicsView>
<GraphicsView.Drawable>
<!-- this binding does not -->
<drawables:ScoreGaugeDrawable
Score = "{Binding AvgScore}"/>
</GraphicsView.Drawable>
</GraphicsView>
</VerticalStackLayout>
</Grid>
</Frame>
</VerticalStackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<ActivityIndicator IsVisible = "{Binding IsBusy}"
IsRunning = "{Binding IsBusy}"
HorizontalOptions = "FillAndExpand"
VerticalOptions = "CenterAndExpand"/>
</Grid>
</ContentPage>
ScoreGaugeDrawable.cs
namespace MediSkillApp.Drawables;
public class ScoreGaugeDrawable : BindableObject, IDrawable
{
public static readonly BindableProperty ScoreProperty = BindableProperty.Create(nameof(Score),
typeof(double),
typeof(ScoreGaugeDrawable));
public double Score {
get => (double)GetValue(ScoreProperty);
set => SetValue(ScoreProperty, value);
}
public void Draw(ICanvas canvas, RectF dirtyRect)
{
var centerPoint = new PointF(32, 0);
var circleRadius = 5;
canvas.FillColor = Colors.Black;
canvas.FillCircle(centerPoint, circleRadius);
canvas.StrokeColor = Colors.Black;
canvas.DrawLine(centerPoint, new Point(0, Score * 10)); //Just draw something for testing
}
}
Процедура.cs
namespace MediSkillApp.Model;
public class Procedure
{
public string Identifier { get; set; }
public DateTime Date { get; set; }
public string GetDate {
get => Date.ToString("d/M-yyyy");
}
public int ProcedureType { get; set; }
public string ProcedureTypeString { get; set; }
public double AvgScore { get; set; }
public string GetAvgScore {
get {
if (AvgScore == 0) return "";
return AvgScore.ToString();
}
}
public int OpRole { get; set; }
public string OpRoleString { get; set; }
public string Icon {
get {
switch (ProcedureType) {
case 7:
return Icons.IconBleed;
case 8:
return Icons.IconTEA;
case 18:
return Icons.IconTEA;
default:
return Icons.IconSurgery;
}
}
}
public bool IsScored => AvgScore > 0;
}
ПроцедурыViewModel.cs
using MediSkillApp.Model;
using MediSkillApp.Services;
namespace MediSkillApp.ViewModel;
public partial class ProceduresViewModel : BaseViewModel
{
public ObservableCollection<Procedure> Procedures { get; } = new();
private APIService APIservice;
public ProceduresViewModel(APIService aPIservice) {
APIservice = aPIservice;
}
[RelayCommand]
public async Task GetProceduresAsync() {
if (IsBusy) return;
try {
IsBusy = true;
var procedures = await APIservice.GetProceduresAsync("8", "dawda", Procedures.Count, 15);
foreach (Procedure procedure in procedures) {
Procedures.Add(procedure);
}
} catch(Exception ex) {
Debug.WriteLine(ex);
} finally {
IsBusy = false;
}
}
public void ClearProcedures() {
Procedures.Clear();
}
}
Вы поставили точку останова в методе Draw()
, чтобы увидеть, как часто он вызывается и каково значение Score
?
Я обновил модель ViewModel и процедуру. Когда я ставлю точку останова в Draw, она вызывается только тогда, когда я этого ожидал, т. е. когда IsScored имеет значение true (определено в procedure.cs), но значение равно 0.
Я смог воспроизвести проблему. Кажется, что Drawables нельзя использовать с BindableProperty
, по крайней мере, это не имеет никакого эффекта, значение свойства не обновляется.
Однако мне удалось найти обходной путь для этой проблемы. Вместо добавления свойства Score
к ScoreGaugeDrawable
вы можете добавить его к GraphicsView
, расширив его через наследование.
Вы можете удалить базовый класс BindableObject
, а также привязываемый ScoreProperty
из ScoreGaugeDrawable
и превратить свойство Score
в обычное свойство с геттером и сеттером по умолчанию:
namespace MediSkillApp.Drawables;
public class ScoreGaugeDrawable : IDrawable
{
public double Score { get; set; }
public void Draw(ICanvas canvas, RectF dirtyRect)
{
var centerPoint = new PointF(32, 0);
var circleRadius = 5;
canvas.FillColor = Colors.Black;
canvas.FillCircle(centerPoint, circleRadius);
canvas.StrokeColor = Colors.Black;
canvas.DrawLine(centerPoint, new Point(0, Score * 10)); //Just draw something for testing
}
}
Затем создайте ScoreGraphicsView
, который наследуется от GraphicsView
, и добавьте к нему связываемый ScoreProperty
:
namespace MediSkillApp.Drawables;
public class ScoreGraphicsView : GraphicsView
{
public double Score
{
get => (double)GetValue(ScoreProperty);
set => SetValue(ScoreProperty, value);
}
public static readonly BindableProperty ScoreProperty = BindableProperty.Create(nameof(Score), typeof(double), typeof(ScoreGraphicsView), propertyChanged: ScorePropertyChanged);
private static void ScorePropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
if (bindable is not ScoreGraphicsView { Drawable: ScoreGaugeDrawable drawable } view)
{
return;
}
drawable.Score = (double)newValue;
view.Invalidate();
}
}
Таким образом, оценка должна быть передана в GraphicsView, который (к сожалению) теперь должен знать о ScoreGaugeDrawable
. Что делает этот код, так это то, что он получает любые обновления привязываемого ScoreProperty
и передает значение ScoreGaugeDrawable
. Если значение изменилось и Drawable
имеет тип ScoreGaugeDrawable
, устанавливается новое значение, а затем представление становится недействительным, что вызывает перерисовку.
Вы можете использовать ScoreGraphicsView
и ScoreGaugeDrawable
вот так в своем XAML:
<drawables:ScoreGraphicsView
Score = "{Binding AvgScore}">
<drawables:ScoreGraphicsView.Drawable>
<drawables:ScoreGaugeDrawable/>
</drawables:ScoreGraphicsView.Drawable>
</drawables:ScoreGraphicsView>
Это не идеально, но должно решить вашу проблему на данный момент. Я проверил это сам в своем репозитории MAUI Samples, и он работает довольно хорошо.
ewerspej, ты гений! Большое спасибо, я был близок к тому, чтобы сойти с ума, и вы сделали мой день. Кстати, в вашем ответе есть несколько опечаток (RadiusProperty должен быть ScoreProperty в «наборе», а ScoreDrawable должен быть ScoreGaugeDrawable, чтобы соответствовать исходному названию).
Спасибо, я исправил это. Это произошло потому, что я сам попробовал решение, а затем скопировал его в ответ, прежде чем корректировать имена.
Где определено свойство
AvgScore
? Я предполагаю, что это вProceduresViewModel
. Вы должны показать соответствующие биты этой ViewModel.