简体   繁体   中英

Xamarin binding Image as binary array within a ListView. Issue while removing entries from the observable collection

I have a xamarin android app which provides the option to make images. Those images will be stored into a database. The existing images will be displayed in a list view (as a preview). You need to be able to delete those images. Everything works fine except the image "removal". I am binding the images via ObservableCollection. Each object has a binary array property named "ImageDocumentData". This property will be used as binding for the "Image.Source". I am using a converter for the binding in order to convert the binary array into a ImageSource (via MemoryStream). It does display everything correctly. However if I have more than one image in the preview listview and I start remove images from the ObservableCollection I am getting the runtime error:

System.ObjectDisposedException: 'Cannot access a closed Stream.'

While rearranging the listview ui during runtime he is not getting into the converter functions again. I am suspecting the following: Each time after adding a new image to the ObservableCollection, the image control is disposing the ImageSource stream. After I remove an entry from the ObservableCollection, he is taking the internal cached ImageSource Object and is trying to read from the same stream again which he already disposed. He needs to get the Image Data again because after I delete an entry from the collection he needs to rearranging (redraw) the listview. I really don't know how to solve that issue. I need to use a stream within the image source because I only have a binary array (no file, no uri possible). This is only my assumption. However I can't see any other reasonable explanation for this runtime error. I've tried a few other options but so far none have worked for me, so I was hoping someone here can give me a hint into the correct direction.

This is my DTO:

    [DataContract]
    public class RequestItemImageDocument : INotifyPropertyChanged
    {

        #region Fields
        private Guid _requestItemImageDocId;
        private Guid _orderItemId;
        private byte[] _imagedocData;
        private string _documentDescription;
        private DateTime? _storageDate;
        private byte _isSynced;
        #endregion

        #region Properties
        [DataMember]
        public Guid RequestItemImageDocumentId 
        {
            get 
            {
                return _requestItemImageDocId;
            }
            set 
            {
                _requestItemImageDocId = value;
                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs("RequestItemImageDocumentId"));
            } 
        }
        [DataMember]
        public Guid OrderItemId 
        {
            get
            {
                return _orderItemId;
            }
            set
            {
                _orderItemId = value;
                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs("OrderItemId"));
            }
        }

        [DataMember]
        public byte[] ImageDocumentData 
        {
            get
            {
                return _imagedocData;
            }
            set
            {
                _imagedocData = value;
                if (PropertyChanged != null) 
                {
                    PropertyChanged(this, new PropertyChangedEventArgs("ImageDocumentData"));

                }
            }
        }
        [DataMember]
        public string DocumentDescription 
        {
            get
            {
                return _documentDescription;
            }
            set
            {
                _documentDescription = value;
                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs("DocumentDescription"));
            }
        }
        [DataMember]
        public DateTime? StorageDate 
        {
            get
            {
                return _storageDate;
            }
            set
            {
                _storageDate = value;
                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs("StorageDate"));
            }
        }
        [DataMember]
        public byte IsSynced 
        {
            get
            {
                return _isSynced;
            }
            set
            {
                _isSynced = value;
                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs("IsSynced"));
            }
        }
        #endregion
        #region Constr
        public RequestItemImageDocument() 
        {

        }
        #endregion
        public event PropertyChangedEventHandler PropertyChanged;
    }

This is my view:

<?xml version="1.0" encoding="utf-8" ?>
<ui:PasAMBasePage
    x:Class="AssetManagement.Mobile.Core.UI.OrderItemImageDocumentationPage"
    xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:actionMenu="clr-namespace:Client.ApplicationDataModel.UI.Controls.ActionMenu;assembly=Client.ApplicationDataModel.UI"
    xmlns:controls="clr-namespace:AssetManagement.Mobile.Core.Controls"
    xmlns:controls1="clr-namespace:UI.XF.Controls;assembly=UI"
    xmlns:AppList="clr-namespace:Client.ApplicationDataModel.UI.Controls.AppList;assembly=Client.ApplicationDataModel.UI"
    xmlns:res="clr-namespace:AssetManagement.Mobile.Core.Resources"
    xmlns:ui="clr-namespace:AssetManagement.Mobile.Core.UI"
    xmlns:valueconverter="clr-namespace:AssetManagement.Mobile.Core.Classes.ValueConverter"
    xmlns:viewmodel="clr-namespace:AssetManagement.Mobile.Core.ViewModels"
    Title="Bild Dokumentation" 
    x:TypeArguments="viewmodel:OrderItemImageDocuViewModel"><!--{res:Translate OrderItemImageDocumentary}-->
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="Auto" />
        </Grid.ColumnDefinitions>
        <StackLayout Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3">
            <ListView ItemsSource="{Binding OrderItemImages}"
          HasUnevenRows="true"
          ItemSelected="OnListViewItemSelected"
          ItemTapped="OnListViewItemTapped" x:Name="ImagePreviewListView">
                <ListView.ItemTemplate>
                    <DataTemplate>
                        <ViewCell>
                            <Grid Padding="10">
                                <Grid.Resources>
                                    <valueconverter:ByteArrayToImageSourceConverter x:Key="SourceConverter" />
                                </Grid.Resources>
                                <Grid.RowDefinitions>
                                    <RowDefinition Height="Auto" />
                                    <RowDefinition Height="*" />
                                </Grid.RowDefinitions>
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition Width="Auto" />
                                    <ColumnDefinition Width="*" />
                                </Grid.ColumnDefinitions>
                           <Image Grid.RowSpan="2"
                           Source="{Binding ImageDocumentData, Converter={StaticResource SourceConverter}}"
                           Aspect="AspectFit"
                           HeightRequest="120"
                           WidthRequest="120" />
                                <Label Grid.Column="1"
                           Text="{Binding DocumentDescription}"
                           FontAttributes="Bold" />
                            </Grid>
                        </ViewCell>
                    </DataTemplate>
                </ListView.ItemTemplate>
            </ListView>
        </StackLayout>
        <StackLayout Grid.Row="1" Grid.ColumnSpan="3" Grid.Column="0" Orientation="Horizontal" HorizontalOptions="End" VerticalOptions="End">
            <controls:ImageButtonControl
                Clicked="btnRemove_Clicked"
                FontSize="30"
                Glyph="trash-alt"
                HeightRequest="60"
                HorizontalOptions="End"/>
            <controls:ImageButtonControl
                Clicked="btnAdd_Clicked"
                FontSize="30"
                Glyph="plus"
                HeightRequest="60"
                HorizontalOptions="End"/>
            <controls:ImageButtonControl
                Clicked="btnSave_Clicked"
                FontSize="30"
                Glyph="save"
                HeightRequest="60"
                HorizontalOptions="End"/>

        </StackLayout>
    </Grid>
</ui:PasAMBasePage>

This is the code behind of the view:

    public partial class OrderItemImageDocumentationPage : PasAMBasePage<OrderItemImageDocuViewModel>
    {
        private IAppConfiguration _appConfig;

        public OrderItemImageDocumentationPage()
        {
            InitializeComponent();
        }

        public OrderItemImageDocumentationPage(OrderItemImageDocuViewModel viewModel, IEventLogService eventLog, ILifetimeScope scope,
            IUserInterfaceService uiService, IAppConfiguration appConfig)
            : base(viewModel, eventLog, scope, uiService)
        {
            InitializeComponent();

            _appConfig = appConfig;

            ViewModel.PropertyChanged += ViewModel_PropertyChanged;
        }

        private void ViewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {
            if (e.PropertyName == nameof(OrderItemListViewModel.Order))
            {
                //orderItemAppList.EventContext = ViewModel;
            }
        }

        private void OnListViewItemTapped(object sender, Xamarin.Forms.ItemTappedEventArgs e)
        {

        }

        private void OnListViewItemSelected(object sender, Xamarin.Forms.SelectedItemChangedEventArgs e)
        {

        }


        private async void btnAdd_Clicked(object sender, EventArgs e)
        {
            RequestItemImageDocument document = new RequestItemImageDocument();
            document.RequestItemImageDocumentId = Guid.NewGuid();
            await CrossMedia.Current.Initialize();
            if (!CrossMedia.Current.IsCameraAvailable ||
    !CrossMedia.Current.IsTakePhotoSupported)
            {
                await DisplayAlert("No Camera", "No camera available.", "OK");
                return;
            }
            var file = await CrossMedia.Current.TakePhotoAsync(new Plugin.Media.Abstractions.StoreCameraMediaOptions
            {
                Directory = "AssetDocumentary",
                SaveToAlbum = true,
                CompressionQuality = 75,
                CustomPhotoSize = 50,
                PhotoSize = PhotoSize.MaxWidthHeight,
                MaxWidthHeight = 2000,
                DefaultCamera = CameraDevice.Front
            });
            if (!string.IsNullOrEmpty(file.Path)) 
            {
                var result = await UserDialogs.Instance.PromptAsync("Fügen Sie eine optionale Textbeschreibung hinzu.", "Beschreibung");
                document.DocumentDescription = result.Text;
                byte[] data = null;
                using (var stream = file.GetStream()) 
                {
                    var fInfo = new System.IO.FileInfo(file.Path);
                    data = new byte[fInfo.Length];
                    stream.Read(data, 0, data.Length);
                }
                document.ImageDocumentData = data;
                base.ViewModel.OrderItemImages.Add(document);

                Android.Content.Context context = Android.App.Application.Context;
                Android.Net.Uri filesUri = Android.Provider.MediaStore.Files.GetContentUri("external");
                string where = Android.Provider.MediaStore.MediaColumns.Data + "=?";
                //System.IO.File.Delete(file.Path);
                context.ContentResolver.Delete(filesUri, where, new string[] { file.Path });
            }

        }

        private void btnSave_Clicked(object sender, EventArgs e)
        {

        }

        private void btnRemove_Clicked(object sender, EventArgs e)
        {
            if (ImagePreviewListView.SelectedItem != null) 
            {
                var selectedItem = ImagePreviewListView.SelectedItem as RequestItemImageDocument;
                var test = ViewModel.OrderItemImages.FirstOrDefault(
                    c => c.RequestItemImageDocumentId.Equals(((RequestItemImageDocument)ImagePreviewListView.SelectedItem)
                    .RequestItemImageDocumentId));
                ImagePreviewListView.SelectedItem = null;
                ViewModel.OrderItemImages.Remove(test);
            }
        }
    }

This is my converter:

    public class ByteArrayToImageSourceConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            ImageSource xValue = null ;
            try 
            {
                if (value != null) 
                {
                    MemoryStream mem = new MemoryStream((byte[])value);
                    xValue = ImageSource.FromStream(new Func<Stream>(() => { return mem; }));
                }

            }catch(Exception ex) 
            {

            }
            return xValue;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotSupportedException();
        }
    }

This is the view model:

    public class OrderItemImageDocuViewModel : ApplicationDataBaseViewModel, INavigationEventHandler
    {
        #region Klassenvariablen

        private readonly int _pagingSize = 20;
        private IAppConfiguration _appConfig;
        private ILifetimeScope _scope;
        private IBarcodeParserService _barcodeParser;
        private IEventLogService _eventLog;
        private IUserDataService _userData;
        private int _itemsToLoadCount;
        private ObservableCollection<RequestItemImageDocument> _orderItemImages = new ObservableCollection<RequestItemImageDocument>();
        private OrderItem _orderItemContext = null;
        #endregion Klassenvariablen

        #region Konstruktor

        public OrderItemImageDocuViewModel(ILifetimeScope scope, IDataRepositoryService dataRepository, IBarcodeParserService barcodeParserService, IEventLogService eventLog, IUserDataService userData, ITranslationService translationService, IAppConfiguration appConfig)
            : base(dataRepository)
        {
            _eventLog = eventLog.Initialize(typeof(OrderItemImageDocuViewModel));
            _scope = scope;
            _appConfig = appConfig;
            _userData = userData;
            _barcodeParser = barcodeParserService;
            TranslationService = translationService;

            _itemsToLoadCount = _pagingSize;


        }

        #endregion Konstruktor

        #region Properties

        [NavigationParameter]
        public Order Order { get; set; }

        public IEventLogService EventLog => _eventLog;

        public bool IsLoading { get; set; }

        public bool DatabasesMissing { get; set; }



        public ITranslationService TranslationService { get; set; }

        public IList<OrderItem> OrderItems { get; set; }

        public IOrderTypeScanHandling ScanHandler { get; set; }

        public ObservableCollection<RequestItemImageDocument> OrderItemImages 
        {
            get 
            {

                return _orderItemImages;
            }
            set 
            {
                _orderItemImages = value;
                base.OnPropertyChanged("OrderItemImages");
            }
        }
        #endregion Properties

        #region Functions

        #endregion

        public override async void OnNavigationParametersProvided(NavigationContext context)
        {
            if (!DatabasesMissing)
            {
                IsLoading = true;

                try
                {
                    //await InitializeData();

                    //await LoadDataOnDemand();
                }
                catch (Exception ex)
                {
                    //ex.TrackError();
                }

                IsLoading = false;
            }
        }

    }

[Update] I have extracted the specific code and put it into a standalone solutions. That solution can be found at https://github.com/Q-Tec90/ImageListTest
In order to reproduce the issue just add 3 images to the listview. Afterwards select the second entry in the listview and click on the remove button. You will get the runtime exception.
I am running it against an android 6 device. The device or emulator needs to have a front camera in order to make the images.

I found the issue. It was located in the converter.

This was my previous converter function. The memory stream needs to be defined within the delegate function. Otherwise when the listview is rerendering the control it will try to read from the old already disposed memory stream.

        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            ImageSource xValue = null ;
            try 
            {
                if (value != null) 
                {
                    MemoryStream mem = new MemoryStream((byte[])value);
                    xValue = ImageSource.FromStream(new Func<Stream>(() => { return mem; }));
                }

            }catch(Exception ex) 
            {

            }
            return xValue;
        }

Fixed Version:

        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            ImageSource xValue = null ;
            try 
            {
                if (value != null) 
                {

                    xValue = ImageSource.FromStream(
                        new Func<Stream>(() => 
                        {
                            MemoryStream mem = new MemoryStream((byte[])value);
                            return mem; 
                        }));
                }

            }catch(Exception ex) 
            {

            }
            return xValue;
        }

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM