First off I would like to point out that I have raised this as a bug with Microsoft but they are unwilling to fix it at this point in time. What I am looking for is a workaround or a better way of achieving what I am trying to do as our customer has deemed this a rather important issue.
The code
MainWindow.xaml
<Grid x:Name="mainGrid">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ListBox ItemsSource="{Binding Images}">
<ListBox.ItemTemplate>
<DataTemplate>
<Image Source="{Binding}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button Content="Print to file" Grid.Row="1" Click="PrintToFile_Click"/>
<Button Content="Print to device" Grid.Row="2" Click="PrintToDevice_Click"/>
</Grid>
MainWindow.xaml.cs
public partial class MainWindow : Window
{
public IList<byte[]> Images { get; set; }
public MainWindow()
{
InitializeComponent();
Assembly currentAssembly = Assembly.GetExecutingAssembly();
this.Images = new List<byte[]>
{
ReadToEnd(currentAssembly.GetManifestResourceStream("PrintingInvestigation.Images.Chrysanthemum.jpg")),
ReadToEnd(currentAssembly.GetManifestResourceStream("PrintingInvestigation.Images.Desert.jpg")),
ReadToEnd(currentAssembly.GetManifestResourceStream("PrintingInvestigation.Images.Hydrangeas.jpg")),
};
this.DataContext = this;
}
public static byte[] ReadToEnd(System.IO.Stream stream)
{
long originalPosition = 0;
if (stream.CanSeek)
{
originalPosition = stream.Position;
stream.Position = 0;
}
try
{
byte[] readBuffer = new byte[4096];
int totalBytesRead = 0;
int bytesRead;
while ((bytesRead = stream.Read(readBuffer, totalBytesRead, readBuffer.Length - totalBytesRead)) > 0)
{
totalBytesRead += bytesRead;
if (totalBytesRead == readBuffer.Length)
{
int nextByte = stream.ReadByte();
if (nextByte != -1)
{
byte[] temp = new byte[readBuffer.Length * 2];
Buffer.BlockCopy(readBuffer, 0, temp, 0, readBuffer.Length);
Buffer.SetByte(temp, totalBytesRead, (byte)nextByte);
readBuffer = temp;
totalBytesRead++;
}
}
}
byte[] buffer = readBuffer;
if (readBuffer.Length != totalBytesRead)
{
buffer = new byte[totalBytesRead];
Buffer.BlockCopy(readBuffer, 0, buffer, 0, totalBytesRead);
}
return buffer;
}
finally
{
if (stream.CanSeek)
{
stream.Position = originalPosition;
}
}
}
private void PrintToDevice_Click(object sender, RoutedEventArgs e)
{
PrintDialog dialog = new PrintDialog();
if (dialog.ShowDialog() == true)
{
Thickness pageMargins;
if (dialog.PrintTicket.PageBorderless.HasValue == true)
{
if (dialog.PrintTicket.PageBorderless.Value == PageBorderless.Borderless)
{
pageMargins = new Thickness(0, 0, 0, 0);
}
else
{
pageMargins = new Thickness(20, 20, 20, 20);
}
}
else
{
pageMargins = new Thickness(20, 20, 20, 20);
}
int dpiX = 300;
int dpiY = 300;
if (dialog.PrintTicket.PageResolution != null &&
dialog.PrintTicket.PageResolution.X.HasValue &&
dialog.PrintTicket.PageResolution.Y.HasValue)
{
dpiX = dialog.PrintTicket.PageResolution.X.Value;
dpiY = dialog.PrintTicket.PageResolution.Y.Value;
}
else
{
dialog.PrintTicket.PageResolution = new PageResolution(dpiX, dpiY);
}
VisualDocumentPaginator paginator = new VisualDocumentPaginator(this.mainGrid, this.mainGrid.ActualWidth);
paginator.PageSize = new Size(dialog.PrintableAreaWidth, dialog.PrintableAreaHeight);
dialog.PrintDocument(paginator, "My first print");
GC.Collect();
}
}
private void PrintToFile_Click(object sender, RoutedEventArgs e)
{
string filePath = this.PrintToFile(null, this.mainGrid, "My first print", this.mainGrid.ActualHeight, this.mainGrid.ActualWidth);
Process.Start(filePath);
}
public string PrintToFile(Visual titleVisual, Visual contentVisual, string title, double bottomMost, double rightMost)
{
string printedFilePath = System.IO.Path.Combine(System.IO.Path.GetTempPath(), string.Format(CultureInfo.InvariantCulture, "{0}.xps", title));
XpsDocument printedDocument = new XpsDocument(printedFilePath, FileAccess.Write, System.IO.Packaging.CompressionOption.SuperFast);
VisualDocumentPaginator paginator = new VisualDocumentPaginator(contentVisual as FrameworkElement, rightMost);
paginator.PageSize = new Size(793.7, 1122.5);
XpsDocumentWriter writer = XpsDocument.CreateXpsDocumentWriter(printedDocument);
writer.Write(paginator, new PrintTicket
{
Collation = Collation.Collated,
CopyCount = 1,
DeviceFontSubstitution = DeviceFontSubstitution.On,
Duplexing = Duplexing.OneSided,
InputBin = InputBin.AutoSelect,
OutputColor = OutputColor.Color,
OutputQuality = OutputQuality.High,
PageMediaSize = new PageMediaSize(PageMediaSizeName.ISOA4),
PageOrientation = PageOrientation.Portrait,
PageResolution = new PageResolution(PageQualitativeResolution.High),
PagesPerSheet = 1,
TrueTypeFontMode = TrueTypeFontMode.Automatic
});
printedDocument.Close();
return printedFilePath;
}
}
VisualDocumentPaginator.cs
public class VisualDocumentPaginator : DocumentPaginator
{
#region Fields
private double desiredWidth;
private FrameworkElement element;
#endregion
#region Properties
public int Columns
{
get
{
return 1;// (int)Math.Ceiling(Element.ActualWidth / PageSize.Width);
}
}
public int Rows
{
get
{
return (int)Math.Ceiling(element.ActualHeight / PageSize.Height);
}
}
#endregion
#region Constructors
public VisualDocumentPaginator(FrameworkElement element, double desiredWidth)
{
this.desiredWidth = desiredWidth;
this.element = element;
}
#endregion
#region DocumentPaginator Members
public override DocumentPage GetPage(int pageNumber)
{
TransformGroup transforms = new TransformGroup();
double scaleRatio = this.PageSize.Width / this.desiredWidth;
int row = (pageNumber / Columns);
double pageHeight = -PageSize.Height * row / scaleRatio;
double pageWidth = -PageSize.Width * (pageNumber % Columns);
transforms.Children.Add(new TranslateTransform(pageWidth, pageHeight));
// Make sure the control is stretched to fit the page size.
if (scaleRatio != double.NaN)
{
ScaleTransform st = new ScaleTransform(scaleRatio, scaleRatio);
transforms.Children.Add(st);
}
element.RenderTransform = transforms;
Size elementSize = new Size(this.desiredWidth, element.ActualHeight);
element.Measure(elementSize);
element.Arrange(new Rect(new Point(0, 0), elementSize));
var page = new DocumentPage(element, this.PageSize, new Rect(), new Rect());
element.RenderTransform = null;
return page;
}
public override bool IsPageCountValid
{
get { return true; }
}
public override int PageCount
{
get
{
return Columns * Rows;
}
}
public override Size PageSize { set; get; }
public override IDocumentPaginatorSource Source
{
get { return null; }
}
#endregion
}
Apologies for posting all the code but it covers all the areas in which I am seeing the issue. If it helps here is the Microsoft bug report which has a sample project attached where the issue can be reproduced.
The problem
The issue is only seen when writing to an XPS file where only the first image is printed 3 times, if the "Print to device" button is clicked then the correct images are printed.
The reason why I am binding to a byte[] is because I am persisting my images in a local SQL CE database. We store them in a DB because they are only small ~2KB each plus we allow users to import their own Icons into the system to use and we wanted a mechanism to guarantee that they wouldn't be accidentally deleted.
NOTE
I have noticed that if I do not bind to the byte[] as mentioned above then I do not see the issue. Given the fact that the system currently works off the approach of storing the images in a DB I would prefer to stick with it if there is workaround however I am not entirely against replacing the storage mechanism for these images.
I have experienced a similar issue where the first image was duplicated and replaced all other images. In my case, printing to a device, to a XPS document or PDF document did not matter, the issue was still there.
Analysis
I have used a .NET assembly decompiler to figure out how the System.Windows.Xps.XpsDocumentWriter class deals with images to find out if the issue was in my code or in the framework's code. I discovered that the framework uses dictionnairies to import resources such as images into the document. Even if images are imported only once in the XPS document, the document is allowed to reference them many times.
In my case, I was able to figure out that the issue was located in the System.Windows.Xps.Serialization.ImageSourceTypeConverter.GetBitmapSourceFromImageTable method. When the image is built from a System.Windows.Media.Imaging.BitmapFrame, the converter will look for the resource in one of its dictionary using a key. In this case, the key correspond to the hash code of the string returned by the BitmapFrame.Decoder.ToString() method. Unfortunately, since my images are built from byte arrays instead of an URI, the decoder's ToString method returns "image". Since that string will always generate the same hash code no matter the image, the ImageSourceTypeConverter will consider that all images are already added to the XPS document and will return the Uri of the first and only image to be use. This explains why the first image is duplicated and is replacing all other images.
Attempt
My first attempt to workaround the issue was to override the System.Windows.Media.Imaging.BitmapDecoder.ToString() method. In order to do so, I tried to wrap the BitmapFrame and BitmapDecoder into my own BitmapFrame and BitmapDecoder. Unfortunately, the BitmapDecoder class contains an internal abstract method I cannot defined. Since I could not create my own BitmapDecoder, I could not implement that workaround.
Workaround
As mentionned previously, the System.Windows.Xps.Serialization.ImageSourceTypeConverter.GetBitmapSourceFromImageTable method will look for an hash code in a dictionary when the BitmapSource is a BitmapFrame. When it is not a BitmapFrame, it will instead generate a CRC value based on the image binary data and look for it in another dictionary.
In my case, I decided to wrap the BitmapFrame that were generated from byte arrays by the System.Windows.Media.ImageSourceConverter into another type of BitmapSource such as System.Windows.Media.Imaging.CachedBitmap. Since I did not really wanted cached bitmap, I created the CachedBitmap will the following options:
var imageSource = new CachedBitmap( bitmapFrame, BitmapCreateOptions.None, BitmapCacheOption.None );
With these options, the CachedBitmap is mostly a simple BitmapSource wrapper. In my case, this workaround solved my issue.
I built a custom printing solution for a document management system built on WPF. I started using the System.Printing namespace but found endless bugs in .NET that Microsoft had not resolved in a long time. With no possible workarounds I ended up using the more mature System.Drawing.Printing namespace built for Windows Forms, with which I found no issues.
It would probably take you some time to rewrite your code to System.Drawing.Printing, but you are far less likely to hit a brick wall somewhere along the way.
Related
First time trying to render something and I have big troubles... I am using DirectN library and SwapChainSurface class from KlearTouch.MediaPlayer. I am trying to render BGRA32 frame using D3D11Device.
For this I have slightly modified OnNewSurfaceAvailable:
public void OnNewSurfaceAvailable2(Action<ID3D11Device, ID3D11DeviceContext> updateSurface)
{
if (rendering)
{
return;
}
try
{
if (this.swapChain is null || swapChainComObject is null)
{
return;
}
swapChainComObject.GetDesc(out var swapChainDesc).ThrowOnError();
if (swapChainDesc.BufferDesc.Width != PanelWidth || swapChainDesc.BufferDesc.Height != PanelHeight)
{
swapChainComObject.ResizeBuffers(2, PanelWidth, PanelHeight, DXGI_FORMAT.DXGI_FORMAT_UNKNOWN, 0).ThrowOnError();
}
var device = swapChain.Object.GetDevice1().Object.As<ID3D11Device>();
device.GetImmediateContext(out var context);
// context.ClearRenderTargetView(renderTargetView.Object, new []{0f, 1f, 1f, 1f});
updateSurface(device, context);
swapChainComObject.Present(1, 0).ThrowOnError();
}
catch (ObjectDisposedException)
{
Reinitialize();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine("\nException: " + ex, nameof(SwapChainSurface) + '.' + nameof(OnNewSurfaceAvailable));
}
rendering = false;
}
OnSurfaceAvailable2 is called from:
void VideoFrameArrived(Bgra32VideoFrame frame)
{
DispatcherQueue.TryEnqueue(() =>
{
previewSurface.OnNewSurfaceAvailable2((device, context) =>
{
var size = frame.m_height * frame.m_height * 4;
D3D11_TEXTURE2D_DESC td;
td.ArraySize = 1;
td.BindFlags = (uint) D3D11_BIND_FLAG.D3D11_BIND_SHADER_RESOURCE;
td.Usage = D3D11_USAGE.D3D11_USAGE_DYNAMIC;
td.CPUAccessFlags = (uint) D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_WRITE;
td.Format = DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM;
td.Height = (uint) frame.m_height;
td.Width = (uint) frame.m_width;
td.MipLevels = 1;
td.MiscFlags = 0;
td.SampleDesc.Count = 1;
td.SampleDesc.Quality = 0;
D3D11_SUBRESOURCE_DATA srd;
srd.pSysMem = frame.m_pixelBuffer;
srd.SysMemPitch = (uint) frame.m_height;
srd.SysMemSlicePitch = 0;
var texture = device.CreateTexture2D<ID3D11Texture2D>(td, new []{srd});
var mappedResource = context.Map(texture.Object, 0, D3D11_MAP.D3D11_MAP_WRITE_DISCARD);
var mappedData = mappedResource.pData;
unsafe
{
Buffer.MemoryCopy(frame.m_pixelBuffer.ToPointer(), mappedData.ToPointer(), size, size);
}
// Just for debug
var pixelsInFrame = new byte[size];
var pixelsInResource = new byte[size];
Marshal.Copy(frame.m_pixelBuffer, pixelsInFrame, 0, size);
Marshal.Copy(mappedResource.pData, pixelsInResource, 0, size);
context.Unmap(texture.Object, 0);
});
});
}
Problem is that I can't see anything rendered and surface stay black and I assume it should not be.
Update: Project repository
Update 2:
I solved my issue. I had too little knowledge about DX11 so I had to study more how things work there. With this knowledge I updated repository which can display preview from black magic design card. It is just example with many issues so be careful and feel free to look for or inspiration there.
There's a various amount of issues here.
First on frame arrived, you have
var texture = device.CreateTexture2D<ID3D11Texture2D>(td, new []{srd});
So your create a texture, but you do not use it anywhere, it needs to be blitted to the swapchain (can do a CopyResource on device context or draw a full screen triangle/quad).
Note that CopyResource will only work if your swapchain has the same size as your incoming texture, which is rather unlikely, so you will have to draw a blit with a shader most likely.
Also you actually copying the data in the texture twice :
var texture = device.CreateTexture2D<ID3D11Texture2D>(td, new []{srd});
Since you provide initial data, the content is already there.
also, pitch is incorrect :
srd.SysMemPitch = (uint) frame.m_height;
pitch is the length (in bytes) of a line, so it should be :
srd.SysMemPitch = frame.GetRowBytes();
Please also note that in case of a non converted Decklink frame,
GetRowBytes can be different from width*4 (they can align row size to multiple of 16/32 or other values).
Next, in the case of resource map, the following is also incorrect :
unsafe
{
Buffer.MemoryCopy(frame.m_pixelBuffer.ToPointer(), mappedData.ToPointer(), size, size);
}
You are not checking the pitch/stride requirement of a texture (which can be different as well),
so you need to do :
if (mappedResource.RowPitch == frame.GetRowBytes())
{
//here you can use a direct copy as above
}
else
{
//here you need to copy data line per line
}
I am using a UltraExpandableGroupBox in my WinForms application. And I am using the Office2003 style with it. However, I would like to reverse the Expanded and Collapsed Indicator images used. I tried to export the images from the .isl file, but these images don't seems to be among the images exported. How do I access these images?
When the ViewStyle property for the UltraExpandableGroupBox control is set to the GroupBoxViewStyle.Office2003 the Expanded/Collapsed indicator uses an embedded bitmap.
Code below demonstrates how this bitmap can be obtained from the assembly on the runtime and can be used to reverse the current Expanded/Collapsed indicators:
private void ReverseImage_Click(object sender, EventArgs e)
{
var imageName = "GroupBox.ExpansionIndicator_Chevron.bmp";
System.IO.Stream stream = typeof(UltraExpandableGroupBox).Module.Assembly.GetManifestResourceStream(typeof(UltraExpandableGroupBox), imageName);
if (stream != null)
{
// The source bitmap has 7x10px size.
var image = Bitmap.FromStream(stream);
// Converting the image to 16x16 pixels
ultraExpandableGroupBox1.ExpansionIndicatorExpanded = ResizeImage(image, 16, 16);
// Rotation
using (var bmp = new Bitmap(image))
{
bmp.RotateFlip(RotateFlipType.Rotate180FlipNone);
image = bmp.Clone() as Image;
// Exporting bitmap to a file
bmp.Save(#".\" + imageName, ImageFormat.Bmp);
}
ultraExpandableGroupBox1.ExpansionIndicatorCollapsed = ResizeImage(image, 16, 16);
}
}
public static Image ResizeImage(Image image, int new_height, int new_width)
{
var dest = new Bitmap(new_width, new_height);
var g = Graphics.FromImage(dest);
g.InterpolationMode = InterpolationMode.High;
g.DrawImage(image, (dest.Width - image.Width)/2, (dest.Height-image.Height)/2);
return dest;
}
Exported to a file the Expanded/Collapsed indicator bitmap looks like on the picture below:
You can achieve this with simple DrawFilter. Set to your UltraExpandableGroupBox DraFilter property like this:
this.myUltraExpandableGroupBox.DrawFilter = new MyDrawFilter(expandedIndicator, collapsedInidcator);
Then create a new class named MyDrawFilter and let it inherit IUIElementDrawFilter. Your draw filter class may look like this:
public class MyDrawFilter : IUIElementDrawFilter
{
Image expandedIndicator;
Image collapsedIndicator;
public MyDrawFilter(Image expandedIndicator, Image collapsedInidcator)
{
this.expandedIndicator = expandedIndicator;
this.collapsedIndicator = collapsedInidcator;
}
public bool DrawElement(DrawPhase drawPhase, ref UIElementDrawParams drawParams)
{
if (drawParams.Element is GroupBoxExpansionIndicatorUIElement)
{
// if groupbox is expanded change the image with one provided in the constructor
// as expandedIndicator
if ((drawParams.Element.Control as UltraExpandableGroupBox).Expanded)
{
(drawParams.Element.ChildElements[0] as ImageUIElement).Image = this.expandedIndicator;
}
// else gropbox is collapsed change the image with one provided in the constructor
// as collapsedIndicator
else
{
(drawParams.Element.ChildElements[0] as ImageUIElement).Image = this.collapsedIndicator;
}
}
return false;
}
public DrawPhase GetPhasesToFilter(ref UIElementDrawParams drawParams)
{
// filter when GroupBoxExpansionIndicatorUIElement should be drawn. This element has
// one child UIElement of ImageUIElement type. This UIElement holds the expansion
// indicator image.
if (drawParams.Element is GroupBoxExpansionIndicatorUIElement)
{
// we return BeforeDrawChildeElements in order to be able to change the image
return DrawPhase.BeforeDrawChildElements;
}
return DrawPhase.None;
}
}
I built out this whole app thinking that the garbage collector handled memory clean-up just fine, which was incredibly stupid and naive of me, but hey, it was my first time every using Xamarin to build an app, and my first time ever building an app, so what's a guy to do? Every screen seems to leak memory, but the screens that leak the most are screens that have bitmaps, generating a memory dump and analyzing it in MAT, I found the following:
So there are 4 potential culprits, 2 are bitmaps, 2 are byte arrays. This is a heap dump for the main menu of the app, if I go into my list view activity for listing out elements, I get 5 potential leaks from bitmaps. Here is the code for the activity:
AssetManager assets = Assets;
Window.AddFlags(WindowManagerFlags.DrawsSystemBarBackgrounds);
var topPanel = FindViewById<TextView>(Resource.Id.topPanel);
topPanel.Text = service.GetLanguageValue("use recommendations - top bar heading");
topPanel.Dispose();
var lowerPanel = FindViewById<TextView>(Resource.Id.recommendationsPanel);
lowerPanel.Text = service.GetLanguageValue("title upper - recommendations by variety");
Shared.ScaleTextToOneLine(lowerPanel, lowerPanel.Text, Shared.ScaleFloatToDensityPixels(Shared.GetViewportWidthInDp()), 1.0f);
lowerPanel.Dispose();
// Read html file and replace it's contents with apple data
string html = "";
using (StreamReader sr = new StreamReader(Assets.Open("apple-variety-detail.html")))
{
html = sr.ReadToEnd();
}
html = ReplaceAppleDetailsHtml(html);
var webview = FindViewById<WebView>(Resource.Id.recommendationsMessage);
CleanWebView();
webview.LoadDataWithBaseURL("file:///android_asset/",
html,
"text/html", "UTF-8", null);
if (Shared.currentApple != null)
{
// Setup apple image
using (var imageView = FindViewById<ImageView>(Resource.Id.recommendationsImage))
{
var apple = this.apples.Where(a => a.Id == Shared.currentApple.AppleId).Select(a => a).First();
var imgName = apple.Identifier.First().ToString().ToUpper() + apple.Identifier.Substring(1);
var fullImageName = "SF_" + imgName;
using (var bitmap = Shared.decodeSampledBitmapFromResource(ApplicationContext.Resources,
Resources.GetIdentifier(fullImageName.ToLower(), "drawable", PackageName),
200, 200))
{
imageView.SetImageBitmap(bitmap);
}
}
// Setup apple name
FindViewById<TextView>(Resource.Id.appleNameTextView).Text = Shared.currentApple.Name;
}
else
{
FindViewById<TextView>(Resource.Id.appleNameTextView).Text = "Not Found!";
}
// Setup list menu for apples
AppleListView = FindViewById<ListView>(Resource.Id.ApplesListMenu);
// Scale details and list to fit on the same screen if the screen size permits
if (Shared.GetViewportWidthInDp() >= Shared.minPhoneLandscapeWidth)
{
var listViewParams = AppleListView.LayoutParameters;
// Scales list view to a set width
listViewParams.Width = Shared.ScaleFloatToDensityPixels(240);
listViewParams.Height = Shared.ScaleFloatToDensityPixels(Shared.GetViewportHeightInDp());
AppleListView.LayoutParameters = listViewParams;
}
else
{
// Here, we either need to hide the list view if an apple was selected,
// or set it to be 100% of the screen if it wasn't selected.
if(!Shared.appleSelected)
{
var listViewParams = AppleListView.LayoutParameters;
// Scales list view to a set width
listViewParams.Width = Shared.ScaleFloatToDensityPixels(Shared.GetViewportWidthInDp());
listViewParams.Height = Shared.ScaleFloatToDensityPixels(Shared.GetViewportHeightInDp());
AppleListView.LayoutParameters = listViewParams;
}
else
{
var listViewParams = AppleListView.LayoutParameters;
// Scales list view to a set width
listViewParams.Width = Shared.ScaleFloatToDensityPixels(0);
listViewParams.Height = Shared.ScaleFloatToDensityPixels(Shared.GetViewportHeightInDp());
AppleListView.LayoutParameters = listViewParams;
}
}
// Set listview adapter
if(AppleListView.Adapter == null)
{
AppleListView.Adapter = new Adapters.AppleListAdapter(this, (List<Apple>)apples, this);
}
AppleListView.FastScrollEnabled = true;
// Set the currently active view for the slide menu
var frag = (SlideMenuFragment)FragmentManager.FindFragmentById<SlideMenuFragment>(Resource.Id.SlideMenuFragment);
frag.SetSelectedLink(FindViewById<TextView>(Resource.Id.SlideMenuRecommendations));
// Replace fonts for entire view
Typeface tf = Typeface.CreateFromAsset(assets, "fonts/MuseoSansRounded-300.otf");
FontCrawler fc = new FontCrawler(tf);
fc.replaceFonts((ViewGroup)this.FindViewById(Android.Resource.Id.recommendationsRootLayout));
tf.Dispose();
}
The important part to note about this is the way this activity works is it loads an adapter, and when it displays it shows a list of items, when an item is clicked, it reloads this same activity, and it computes the screen size, shrinks down the list to show only the webview off to the side, and displays details about the item, thus simulating 2 screens, the reason I did this is because when the screen size is larger, it needs to show all of this as one single view, so on larger screens it will actually show both the listview and the webview, but still reload the activity to load new data.
The adapter code is probably what is giving me a hard time, but I'm not sure, I've tried quite a few things, but nothing seems to help, here's the adapter code:
public class AppleListAdapter : BaseAdapter<Apple>
{
List<Apple> items;
Activity context;
ApplicationService service = AgroFreshApp.Current.ApplicationService;
private Context appContext;
private Typeface tf;
static AppleRowViewHolder holder = null;
public AppleListAdapter(Activity context, List<Apple> items, Context appContext): base ()
{
this.context = context;
this.items = items;
this.appContext = appContext;
context.FindViewById<ListView>(Resource.Id.ApplesListMenu).ChoiceMode = ChoiceMode.Single;
tf = Typeface.CreateFromAsset(context.Assets, "fonts/MuseoSansRounded-300.otf");
}
public override long GetItemId(int position)
{
return position;
}
public override Apple this[int position]
{
get { return items[position]; }
}
public override int Count
{
get
{
return items.Count;
}
}
public override View GetView(int position, View convertView, ViewGroup parent)
{
var item = items[position];
var view = convertView;
var imgName = item.Identifier.First().ToString().ToUpper() + item.Identifier.Substring(1);
var fullImageName = "SF_" + imgName;
if (view == null)
{
view = context.LayoutInflater.Inflate(Resource.Layout.appleRowView, null);
}
if (view != null)
{
holder = view.Tag as AppleRowViewHolder;
}
if(holder == null)
{
holder = new AppleRowViewHolder();
view = context.LayoutInflater.Inflate(Resource.Layout.appleRowView, null);
holder.AppleImage = view.FindViewById<ImageView>(Resource.Id.iconImageView);
holder.AppleName = view.FindViewById<TextView>(Resource.Id.nameTextView);
view.Tag = holder;
}
using (var bitmap = Shared.decodeSampledBitmapFromResource(context.Resources,
context.Resources.GetIdentifier(fullImageName.ToLower(), "drawable", context.PackageName),
25, 25))
{
holder.AppleImage.SetImageBitmap(bitmap);
}
holder.AppleName.Text = AgroFreshApp.Current.AppleDetailManager.GetAll().Where(a => a.AppleId == item.Id).Select(a => a.Name).FirstOrDefault();
holder.AppleName.SetTypeface(tf, TypefaceStyle.Normal);
view.Click += (object sender, EventArgs e) =>
{
var apple = AgroFreshApp.Current.AppleManager.Get(item.Id);
Shared.currentApple = AgroFreshApp.Current.AppleDetailManager.GetAll().Where(a=>a.AppleId == item.Id && a.LanguageId == service.UserSettings.LanguageId).Select(a=>a).FirstOrDefault();
Shared.appleSelected = true;
Intent intent = new Intent(appContext, typeof(RecommendationsActivity));
intent.SetFlags(flags: ActivityFlags.NoHistory | ActivityFlags.NewTask);
appContext.StartActivity(intent);
};
return view;
}
}
So I'm using the viewholder pattern here, and assigning click events to each list item as they get generated, with nohistory and newtask as the intent flags so that the pages refreshes properly. To clean up the bitmaps, I have been using these two methods:
This cleans the large image on the details webview:
public void CleanBitmap()
{
// Clean recommendations bitmap
ImageView imageView = (ImageView)FindViewById(Resource.Id.recommendationsImage);
Drawable drawable = imageView.Drawable;
if (drawable is BitmapDrawable)
{
BitmapDrawable bitmapDrawable = (BitmapDrawable)drawable;
if (bitmapDrawable.Bitmap != null)
{
Bitmap bitmap = bitmapDrawable.Bitmap;
if (!bitmap.IsRecycled)
{
imageView.SetImageBitmap(null);
bitmap.Recycle();
bitmap = null;
}
}
}
Java.Lang.JavaSystem.Gc();
}
And this cleans the bitmaps stored in each listview item:
public void CleanListViewBitmaps()
{
var parent = FindViewById<ListView>(Resource.Id.ApplesListMenu);
// Clean listview bitmaps
for (int i = 0; i < parent.ChildCount; i++)
{
var tempView = parent.GetChildAt(i);
// If the tag is null, this no longer holds a reference to the view, so
// just leave it.
if(tempView.Tag != null)
{
AppleRowViewHolder tempHolder = (AppleRowViewHolder)tempView.Tag;
var imageView = tempHolder.AppleImage;
var drawable = imageView.Drawable;
if (drawable is BitmapDrawable)
{
BitmapDrawable bitmapDrawable = (BitmapDrawable)drawable;
if (bitmapDrawable.Bitmap != null)
{
Bitmap bitmap = bitmapDrawable.Bitmap;
if (!bitmap.IsRecycled)
{
imageView.SetImageBitmap(null);
bitmap.Recycle();
bitmap = null;
}
}
}
}
}
Java.Lang.JavaSystem.Gc();
}
They then get called in the activities ondestroy method like so:
protected override void OnDestroy()
{
base.OnDestroy();
CleanBitmap();
CleanListViewBitmaps();
Shared.appleSelected = false;
}
I'm also using a shared class with static variables to essentially track view states like if something was selected or no, but it only stores primitives, it doesn't store any view objects or anything like that, so I don't think that is the problem like I said it looks like bitmaps aren't getting cleaned correctly, and it seems to happen on every view, but this one in particular is bad.
I also on each view load 2 fragments, one is a slide menu fragment in a frame layout, and the other is a navbar fragment that just holds 2 bitmaps for a logo and menu handle, so those could be culprits too I suppose. Here's the navbar fragment:
public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
// Use this to return your custom view for this Fragment
// return inflater.Inflate(Resource.Layout.YourFragment, container, false);
var view = inflater.Inflate(Resource.Layout.navbar, container, false);
var navLogo = view.FindViewById(Resource.Id.navbarLogo);
var menuHandle = view.FindViewById(Resource.Id.menuHandle);
var navSpacer = view.FindViewById(Resource.Id.navSpacer);
((ImageButton)(menuHandle)).SetMaxWidth(Shared.GenerateProportionalWidth(.25f, 50));
((ImageButton)(menuHandle)).SetMaxHeight(Shared.GenerateProportionalHeight(.25f, 50));
((ImageButton)(menuHandle)).Click += (object sender, EventArgs e) =>
{
var slideMenu = FragmentManager.FindFragmentById(Resource.Id.SlideMenuFragment);
if (slideMenu.IsHidden)
{
FragmentManager.BeginTransaction().Show(slideMenu).Commit();
}
else if (!slideMenu.IsHidden)
{
FragmentManager.BeginTransaction().Hide(slideMenu).Commit();
}
};
var navLogoParams = navLogo.LayoutParameters;
// Account for the padding offset of the handle to center logo truly in the center of the screen
navLogoParams.Width = global::Android.Content.Res.Resources.System.DisplayMetrics.WidthPixels - (((ImageButton)(menuHandle)).MaxWidth * 2);
navLogoParams.Height = (Shared.GenerateProportionalHeight(.25f, 30));
navLogo.LayoutParameters = navLogoParams;
// Spacer puts the logo in the middle of the screen, by making it's size the same as the handle on the opposite side to force-center the logo
((Button)(navSpacer)).SetMaxWidth(Shared.GenerateProportionalWidth(.25f, 50));
((Button)(navSpacer)).SetMaxHeight(Shared.GenerateProportionalHeight(.25f, 50));
return view;
}
Does anyone see any obvious or stupid mistake that I'm making? I feel like it has to just be sheer inexperience that's causing me to miss something really obvious, or I'm doing something completely wrong, either way.
EDIT #1:
1 of the bitmaps leaking was the menu handle button in the navigation fragment, so that drops the leak down from 300kb to 200kb, but I still need to figure out how to clean it properly.
EDIT #2:
Here is my code that scales bitmaps down
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
int reqWidth, int reqHeight)
{
// First decode with inJustDecodeBounds=true to check dimensions
BitmapFactory.Options options = new BitmapFactory.Options();
options.InJustDecodeBounds = true;
BitmapFactory.DecodeResource(res, resId, options);
// Calculate inSampleSize
options.InSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// Decode bitmap with inSampleSize set
options.InJustDecodeBounds = false;
return BitmapFactory.DecodeResource(res, resId, options);
}
public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight)
{
// Raw height and width of image
int height = options.OutHeight;
int width = options.OutWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth)
{
int halfHeight = height / 2;
int halfWidth = width / 2;
// Calculate the largest inSampleSize value that is a power of 2 and keeps both
// height and width larger than the requested height and width.
while ((halfHeight / inSampleSize) >= reqHeight
&& (halfWidth / inSampleSize) >= reqWidth)
{
inSampleSize *= 2;
}
}
return inSampleSize;
}
For anyone wondering, I've figured out the problem. Xamarin is a c# wrapper around native java, so at runtime there is the native Java runtime, and the mono runtime as well, so any object like a bitmap that you want to cleanup, you need to cleanup the native Java object, but you also need to clean up the c# handle to the native object, because what happens is the garbage collector goes to see if it should clean your resource, sees a handle associated with the resource, and moves on. My solution was to call the c# dispose after I cleaned up the native Java object, and then call both the c# and Java garbage collector, I'm not sure if calling both garbage collectors is explicitly needed, but I chose to do it anyway. Seriously hope this helps someone out, I do not envy people who have to hunt down these problems.
Sometimes Bitmaps ar not garbage collected correctly, and generete the outofmemory exception.
my suggestion if you're working with bitmaps is to call
System.gc();
to recycle bitmaps from memory correctly
I have a icon which has a few different sizes (16px, 32px, 64px). I am calling ToBitmap() on it, but it is always returning the 32px image. How do I retrieve the 64px one?
Does this help?
Icon sizedIcon = new Icon(Resources.ResourceIcon, new Size(64,64));
For anyone else stumbling upon the same problem, I've found a nice little solution.
Image img = new Icon(Properties.Resources.myIcon, width, height).ToBitmap()
This is a fairly painful limitation in the ResourceManager class. Its GetObject() method doesn't provide a way to pass extra arguments that would allow selecting the returned icon by size. A workaround is to add the icon to the project instead. Use Project + Add Existing Item, select your .ico file. Select the added icon and change the Build Action property to "Embedded Resource".
You can now retrieve the desired icon with code like this:
public static Icon GetIconFromEmbeddedResource(string name, Size size) {
var asm = System.Reflection.Assembly.GetExecutingAssembly();
var rnames = asm.GetManifestResourceNames();
var tofind = "." + name + ".ICO";
foreach (string rname in rnames) {
if (rname.EndsWith(tofind, StringComparison.CurrentCultureIgnoreCase)) {
using (var stream = asm.GetManifestResourceStream(rname)) {
return new Icon(stream, size);
}
}
}
throw new ArgumentException("Icon not found");
}
Sample usage:
var icon1 = GetIconFromEmbeddedResource("ARW04LT", new Size(16, 16));
var icon2 = GetIconFromEmbeddedResource("ARW04LT", new Size(32, 32));
Beware one possible failure mode: this code assumes that the icon was added to the same assembly that contains the method.
The following sets the icon size for all the buttons in the toolbar.
It relies on the resource name being stored in the button tag.
public void SetButtons(object toolstrip, int IconWidth, int IconHeight)
{
var ts = (ToolStrip) toolstrip;
var size = new System.Drawing.Size();
size.Height = IconSize;
size.Width = IconSize;
foreach (ToolStripButton tsBtn in ts.Items)
{
tsBtn.ImageScaling = ToolStripItemImageScaling.None;
var resourcename = (String) tsBtn.Tag;
if (resourcename != null)
{
var myIcon = (Icon) Properties.Resources.ResourceManager.GetObject(resourcename);
var newIcon = new Icon(myIcon, IconWidth, IconHeight);
tsBtn.Image = newIcon.ToBitmap();
}
}
}
The size is determined when you first create the Icon instance, so you need to specify which size you want to use when you create it, using one of the Icon constructors that take a Size parameter.
internal static class IconHelper {
public static Icon GetSize(this Icon icon, int width, int height) {
return icon.GetSize(new Size(width, height));
}
public static Icon GetSize(this Icon icon, Size size) {
using(var mem = new MemoryStream()) {
icon.Save(mem);
mem.Position = 0;
return new Icon(mem, size);
}
}
}
There is no built-in method in the .Net framework that does this.
Instead, you can use this library.
I am developing an app with Mono for Android.
I have been struggling with Out of memory exceptions for the last few days and am starting to lose hope!
I have a ListView displaying anything from 200 to 600 items. These items consist of a bitmap thumbnail and some text.
I am decoding the Bitmap asynchronously using an AsyncTask, here is the code:
public class BitmapWorkerTask : AsyncTask
{
private WeakReference imageViewReference;
public string thisURL = "";
private int sampleSize = 0;
private int reqHeight = 0;
private int reqWidht = 0;
public BitmapWorkerTask(ImageView imageView, int pSampleSize, int pReqWidth, int pReqHeight)
{
//_____________________________________________________________________
// Use a WeakReference to ensure the ImageView can be garbage collected
imageViewReference = new WeakReference(imageView);
reqHeight = pReqHeight;
reqWidht = pReqWidth;
sampleSize = pSampleSize;
}
protected override Java.Lang.Object DoInBackground(params Java.Lang.Object[] #params)
{
string strUrl = #params[0].ToString();
try
{
return DecodeSampleBitmapFromStream(strUrl, reqWidht, reqHeight);
}
catch (Exception ex)
{
return null;
}
}
protected override void OnPostExecute(Java.Lang.Object result)
{
base.OnPostExecute(result);
if (IsCancelled)
{
result = null;
Log.Debug("TT", "OnPostExecute - Task Cancelled");
}
else
{
Bitmap bmpResult = result as Bitmap;
if (imageViewReference != null && bmpResult != null)
{
ImageView view = imageViewReference.Target as ImageView;
if (view != null)
{
view.SetImageBitmap(bmpResult);
}
}
}
}
public static int CalculateInSampleSize( BitmapFactory.Options options, int reqWidth, int reqHeight)
{
//_____________________________
// Raw height and width of image
int height = options.OutHeight;
int width = options.OutWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth)
{
if (width > height)
{
inSampleSize = (int)Math.Round((float)height / (float)reqHeight);
}
else
{
inSampleSize = (int)Math.Round((float)width / (float)reqWidth);
}
}
return inSampleSize;
}
public static Bitmap DecodeSampleBitmapFromStream(string URL, int reqWidth, int reqHeight)
{
URL url = new URL(URL);
try
{
//______________________________________________________________
// First decode with inJustDecodeBounds=true to check dimensions
BitmapFactory.Options options = new BitmapFactory.Options();
options.InJustDecodeBounds = true;
BitmapFactory.DecodeStream(url.OpenConnection().InputStream, null, options);
//______________________
// Calculate inSampleSize
options.InSampleSize = CalculateInSampleSize(options, reqWidth, reqHeight);
//____________________________________
// Decode bitmap with inSampleSize set
options.InJustDecodeBounds = false;
return BitmapFactory.DecodeStream(url.OpenConnection().InputStream, null, options);
}
catch (Exception ex)
{
return null;
}
finally
{
url.Dispose();
}
}
I am starting this AsyncTask from the Lists GetView() function using this Method:
public void loadBitmap(string url, ImageView imageView)
{
if (Common.cancelPotentialWork(url, imageView))
{
BitmapWorkerTask task = new BitmapWorkerTask(imageView, 2, 80,80);
AsyncDrawable asyncDrawable = new AsyncDrawable(null, null, task);
imageView.SetImageDrawable(asyncDrawable);
task.Execute(url);
}
}
Everything works as expected for a period of time, but if I continuously scroll up and down through my list I eventually start getting OutOfMemoryExceptions and the app crashes. My understanding of how the Android list works is it disposes of the ListItem views as they move off screen, but it feels as though this is not happening!
It feels like all those bitmaps I am decoding as I scroll through the list are for whatever reason being held in memory? What could I be missing here that is preventing those bitmaps from being disposed of? Where could I implement a call to Bitmap.Recycle() to ensure the bitmaps are cleared?
I did a test whereby I made a call to GC.Collect on every call to GetView which did seem to keep my memory usage fairly consistent, but I know this shouldn't be needed and it affects scrolling performance.
Why when I scroll through my list without the call to GC.Collect() am I not seeing those garbage collection message indicating that the system is, in fact, doing routine collections?
Any help is appreciated, I am losing the will to code!
My understanding of how the Android list works is it disposes of the List item views as they move off screen, but It feels as though this is not happening!
This isn't correct.
What Android does is it hold on to the set of item views and it tries to reuse them after they have gone off screen. This is what the convertView parameter is for.
I can't see your Adapter code posted in the question, so I'm not sure what your code is for using the convertView parameter, but I'd guess what it should do in the case of a convertView is:
it should cancel any existing async image fetch/conversion
it should start a new one
The MvvmCross code may be a little too complicated for you as a reference/example here, but you can at least see convertView in use in MvxBindableListAdapter.cs - see protected virtual View GetBindableView(View convertView, object source, int templateId)