错误此 PlotModel 已被 OxyPlot 图表中的其他 PlotView 控件使用

Error This PlotModel is already in use by some other PlotView control in OxyPlot chart

我正在使用 Xamarin.Forms OxyPlot 图表。我有一个 collectionview,每个 collectionview 项目都有一个扩展器,每个扩展器里面都有一个 PlotView

<CollectionView x:Name="Kids">
    <CollectionView.ItemTemplate>
        <DataTemplate>
             <xct:Expander Tapped="Expander_Tapped" ClassId="{Binding rowNumber}">
            <xct:Expander.Header>
                <Frame Padding="0" CornerRadius="10" Margin="5" BackgroundColor="White" HasShadow="False">
                    <StackLayout>
                        <Grid BackgroundColor="#f8f8f8">
                            <StackLayout Padding="5" Orientation="Horizontal">
                                <Image x:Name="kidProfile" Source="{Binding image}" WidthRequest="75" HeightRequest="75" HorizontalOptions="Start" Aspect="AspectFill" />
                                <StackLayout Orientation="Vertical">
                                    <Label Text="{Binding first_name}"></Label>
                                    <StackLayout Orientation="Horizontal">
                                        <Label Text="Grade: " FontSize="Small"></Label>
                                        <Label Text="{Binding grade}" FontSize="Small"></Label>
                                    </StackLayout>
                                </StackLayout>
                            </StackLayout>
                            <Image Margin="20" HorizontalOptions="End" Source="arrowDown.png" HeightRequest="15"></Image>
                        </Grid>
                    </StackLayout>
                </Frame>
             </xct:Expander.Header>
             <oxy:PlotView Model="{Binding chart}" HeightRequest="200" WidthRequest="100" />
        </DataTemplate>
    </CollectionView.ItemTemplate>
</CollectionView>

我在 class

中分配 PlotModel
public class ReportsClass
{
    public PlotModel chart
    {
        get
        {
            PlotModel model = new PlotModel();

            CategoryAxis xaxis = new CategoryAxis();
            xaxis.Position = AxisPosition.Bottom;
            xaxis.MajorGridlineStyle = LineStyle.None;
            xaxis.MinorGridlineStyle = LineStyle.None;
            xaxis.MinorTickSize = 0;
            xaxis.MajorTickSize = 0;
            xaxis.TextColor = OxyColors.Gray;
            xaxis.FontSize = 10.0;
            xaxis.Labels.Add("S");
            xaxis.Labels.Add("M");
            xaxis.Labels.Add("T");
            xaxis.Labels.Add("W");
            xaxis.Labels.Add("T");
            xaxis.Labels.Add("F");
            xaxis.Labels.Add("S");
            xaxis.GapWidth = 10.0;
            xaxis.IsPanEnabled = false;
            xaxis.IsZoomEnabled = false;


            LinearAxis yaxis = new LinearAxis();
            yaxis.Position = AxisPosition.Left;
            yaxis.MajorGridlineStyle = LineStyle.None;
            xaxis.MinorGridlineStyle = LineStyle.None;
            yaxis.MinorTickSize = 0;
            yaxis.MajorTickSize = 0;
            yaxis.TextColor = OxyColors.Gray;
            yaxis.FontSize = 10.0;
            yaxis.FontWeight = FontWeights.Bold;
            yaxis.IsZoomEnabled = false;
            yaxis.IsPanEnabled = false;


            ColumnSeries s2 = new ColumnSeries();
            s2.TextColor = OxyColors.White;

            s2.Items.Add(new ColumnItem
            {
                Value = Sunday,
                Color = OxyColor.Parse("#02cc9d")
            });
            s2.Items.Add(new ColumnItem
            {
                Value = Monday,
                Color = OxyColor.Parse("#02cc9d")
            });
            s2.Items.Add(new ColumnItem
            {
                Value = Tuesday,
                Color = OxyColor.Parse("#02cc9d")
            });
            s2.Items.Add(new ColumnItem
            {
                Value = Wednesday,
                Color = OxyColor.Parse("#02cc9d")
            });
            s2.Items.Add(new ColumnItem
            {
                Value = Thursday,
                Color = OxyColor.Parse("#02cc9d")

            });
            s2.Items.Add(new ColumnItem
            {
                Value = Friday,
                Color = OxyColor.Parse("#02cc9d")
            });
            s2.Items.Add(new ColumnItem
            {
                Value = Saturday,
                Color = OxyColor.Parse("#02cc9d")
            });

            model.Axes.Add(xaxis);
            model.Axes.Add(yaxis);
            model.Series.Add(s2);
            model.PlotAreaBorderColor = OxyColors.Transparent;

            return model;
        }
    }
    
}

现在这行得通了,但是在 Expander 中,当我展开一个项目时,PlotView 根本不会显示。所以我将 Class 更改为使用 INotifyPropertyChanged

public class ReportsClass : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public PlotModel chart
    {
        get => _chart;
        set
        {
            _chart = value;
            if(_chart.PlotView == null && value.PlotView == null)
            {
                OnPropertyChanged("chart");
            }

        }
    }
    public PlotModel _chart;

    protected void OnPropertyChanged(string propertyName)
    {
        var handler = PropertyChanged;
        if (handler != null)
            if(this.chart.PlotView == null)
            {
                handler(this, new PropertyChangedEventArgs(propertyName));
            }
    }
}

在我后面的代码中,我使用扩展器的 tapped 方法来填充 PlotView:

void Expander_Tapped(System.Object sender, System.EventArgs e)
{
    if(expander != null)
    {
        expander.IsExpanded = false;
    }
    
    expander = sender as Expander;

    int id = Convert.ToInt32(expander.ClassId);

    ReportsClass item = newKidList[id];

    Device.StartTimer(TimeSpan.FromSeconds(1), () =>
    {
        if (item.chart == null)
        {
            PlotModel model = new PlotModel();

            CategoryAxis xaxis = new CategoryAxis();
            xaxis.Position = AxisPosition.Bottom;
            xaxis.MajorGridlineStyle = LineStyle.None;
            xaxis.MinorGridlineStyle = LineStyle.None;
            xaxis.MinorTickSize = 0;
            xaxis.MajorTickSize = 0;
            xaxis.TextColor = OxyColors.Gray;
            xaxis.FontSize = 10.0;
            xaxis.Labels.Add("S");
            xaxis.Labels.Add("M");
            xaxis.Labels.Add("T");
            xaxis.Labels.Add("W");
            xaxis.Labels.Add("T");
            xaxis.Labels.Add("F");
            xaxis.Labels.Add("S");
            xaxis.GapWidth = 10.0;
            xaxis.IsPanEnabled = false;
            xaxis.IsZoomEnabled = false;


            LinearAxis yaxis = new LinearAxis();
            yaxis.Position = AxisPosition.Left;
            yaxis.MajorGridlineStyle = LineStyle.None;
            xaxis.MinorGridlineStyle = LineStyle.None;
            yaxis.MinorTickSize = 0;
            yaxis.MajorTickSize = 0;
            yaxis.TextColor = OxyColors.Gray;
            yaxis.FontSize = 10.0;
            yaxis.FontWeight = FontWeights.Bold;
            yaxis.IsZoomEnabled = false;
            yaxis.IsPanEnabled = false;


            ColumnSeries s2 = new ColumnSeries();
            s2.TextColor = OxyColors.White;

            s2.Items.Add(new ColumnItem
            {
                Value = item.Sunday,
                Color = OxyColor.Parse("#02cc9d")
            });
            s2.Items.Add(new ColumnItem
            {
                Value = item.Monday,
                Color = OxyColor.Parse("#02cc9d")
            });
            s2.Items.Add(new ColumnItem
            {
                Value = item.Tuesday,
                Color = OxyColor.Parse("#02cc9d")
            });
            s2.Items.Add(new ColumnItem
            {
                Value = item.Wednesday,
                Color = OxyColor.Parse("#02cc9d")
            });
            s2.Items.Add(new ColumnItem
            {
                Value = item.Thursday,
                Color = OxyColor.Parse("#02cc9d")

            });
            s2.Items.Add(new ColumnItem
            {
                Value = item.Friday,
                Color = OxyColor.Parse("#02cc9d")
            });
            s2.Items.Add(new ColumnItem
            {
                Value = item.Saturday,
                Color = OxyColor.Parse("#02cc9d")
            });

            model.Axes.Add(xaxis);
            model.Axes.Add(yaxis);
            model.Series.Add(s2);
            model.PlotAreaBorderColor = OxyColors.Transparent;

            item.chart = model;
        }

        return false;
    });
}

然而,最终我会得到这个错误:

此 PlotModel 已被其他 PlotView 控件使用

现在我明白 PlotView 和它的 PlotModel 之间存在一对一的关系,这给了我们错误,所以我试图检查 PlotModel 是否有 PlotView,但我是仍然收到此错误。

我找到了这个解决方案:

If you set the parent View of your OxyPlot to a DataTemplate that creates a new OxyPlot each time then the OxyPlot cannot be cached. A new PlotModel and PlotView is created each time and this error is avoided (at least that seems to work for me, I am using CarouselView)

https://github.com/oxyplot/oxyplot-xamarin/issues/17

但我不知道如何为集合视图执行此操作,任何帮助将不胜感激。

我也发现了这个:

This is a very common issue of OxyPlot when it's used in MVVM. In OxyPlot, the view and the model are 1-to-1 mapped, when a second view is trying to bind the same PlotModel, you have the issue of "PlotModel is already in use by some other PlotView control". On iOS, ListView's cell will be re-created when it is scrolling. In this case, there will be a newly created view trying to bind the same PlotModel, and then you have the issue. On Android, I guess you will have the same issue too. Try to put your phone from landscape to portrait, see if you have the same issue. In Android, the view will be re-created completely when the orientation is changed.

A quick fix is to break the MVVM design here a little bit. Don't create the model in a separated place but create the model in the view. So whenever a view is re-created by iOS or Android, a new model is also re-created.

https://github.com/oxyplot/oxyplot-xamarin/issues/60

但是我不知道如何应用这部分:

A quick fix is to break the MVVM design here a little bit. Don't create the model in a separated place but create the model in the view. So whenever a view is re-created by iOS or Android, a new model is also re-created.

有关工作版本,请参阅 github ToolmakerSteve / repo OxyplotApp1


“此 PlotModel 已被其他 PlotView 控件使用”

经过对 iOS 的各种测试,我得出结论,在 iOS 上使用(CollectionView 或 ListView)+ Expander + Oxyplot 从根本上来说是不可靠的。

Oxyplot 似乎使 Expander and CollectionView 的已知问题恶化。

因此,最重要的修复是停止使用这些集合视图。将 CollectionView 的使用替换为:

<StackLayout Spacing="0" BindableLayout.ItemsSource="{Binding KidModels}">
    <BindableLayout.ItemTemplate>
        <DataTemplate>

为了获得更好的性能,请进行此更改,以便仅在用户第一次单击给定项目时创建每个图表:

void Expander_Tapped(System.Object sender, System.EventArgs e)
{
    // --- ADD THESE LINES ---
    if (ReferenceEquals(sender, expander)) {
        // User is tapping the existing expander. Don't do anything special.
        return;
    }
    ...
}

注意:还修复了如果用户连续点击 3 次扩展器会立即再次关闭的问题。


第一次单击每个展开器时外观更快。这里有三种选择。从最快到最慢。使用第一个,但如果出现空白图表或其他问题,请切换到第二个。如果第二个仍然有问题,请切换到第三个 - 这是原始的,但延迟时间稍短:

if (item.Chart == null) {
    PlotModel model = CreateReportChart(item);
    Action action = () => {
        item.Chart = model;
    };
    if (false) {
        action();
    } else if (true) {
        Device.BeginInvokeOnMainThread(() => {
            action();
        });
    } else {
        Device.StartTimer(TimeSpan.FromSeconds(0.5), () => {
            action();

            return false;
        });
    }
}

可选:确保扩展器动画在与 Oxyplot 一起使用时不会造成问题。

如果出现问题,但只是“有时”出现,试试这个,看看情况是否有所改善:

<xct:Expander AnimationLength="0" ...>

那应该删除动画。


您可以从与 OnPropertyChanged 相关的代码中删除 if (_chart.PlotView == null && value.PlotView == null) 等测试。也就是说,始终执行 OnPropertyChanged。

原因:将来您可能希望生成经过修改的图,因为数据已更改,并且您进行的测试会阻止 UI 看到更改。