Search code examples
c#xamarin.formsoxyplot

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


I am using Xamarin.Forms OxyPlot Chart. I have a collectionview and in each collectionview item has an expander and inside each of those expanders is a 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>

and I was assigning the PlotModel in my class

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;
        }
    }
    
}

Now this works, but in the Expander when I expand an item the PlotView would not show, at all. So I changed my Class to use 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));
            }
    }
}

And in my code behind I use the expander's tapped method to populate the 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;
    });
}

However, eventually I will get this error:

This PlotModel is already in use by some other PlotView control

Now I understand that there is a one-to-one relation between the PlotView and its PlotModel, which gives us the error, so I have tried to do a check to see if PlotModel has a PlotView, but I am still getting this error.

I found this solution:

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

But I do not know how to do this for a collection view, any help would be much apperciated.

I also found this:

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

But I don't know how to apply this part:

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.


Solution

  • See github ToolmakerSteve / repo OxyplotApp1, for working version.


    "This PlotModel is already in use by some other PlotView control"

    After various tests on iOS, I conclude that using (CollectionView or ListView) + Expander + Oxyplot on iOS is fundamentally not reliable.

    Oxyplot seems to worsen known issues with Expander and CollectionView.

    Therefore, the most important fix is to stop using these collection views. Replace use of CollectionView with:

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

    For better performance, make this change, so each graph is only created the first time a user clicks a given item:

    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;
        }
        ...
    }
    

    NOTE: Also fixes a problem where expander immediately closed again, if user tapped it three times in a row.


    Faster appearance the first time each expander is clicked. Here are three alternatives. From fastest to slowest. Use the first, but if ever get a blank graph or other problem, switch to second. If second still has problems, switch to third - which is the original, though with a slightly shorter time delay:

    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;
            });
        }
    }
    

    OPTIONAL: To be sure expander animation doesn't cause a problem when used with Oxyplot.

    If having problems that occur, but only "sometimes", try this, see if situation improves:

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

    That should remove the animation.


    You can remove tests such as if (_chart.PlotView == null && value.PlotView == null) from your OnPropertyChanged-related code. That is, ALWAYS do the OnPropertyChanged.

    REASON: in the future you might wish to generate a modified plot because data changed, and the tests you have would prevent UI from seeing the change.