UWP GridView Image Stretch bug - c#

I have a GridView control bound to a collection of objects having a BitmapImage property. GridView item template's Image control has a fixed size, while actual pictures may vary in size, can be smaller or bigger than the Image. So i use Stretch=Uniform for bigger pictures, and Stretch=None for smaller. I set the Stretch property on Image_Loaded event:
private void img_ImageOpened(object sender, RoutedEventArgs e)
{
var img = sender as Image;
if (img.Width > (img.Source as BitmapImage).PixelWidth)
{
img.Stretch = Stretch.None;
}
else
{
img.Stretch = Stretch.Uniform;
}
}
So the pics are fitted pretty well:
But if i clear the bound collection and fill it again, things get really messy:
I have spent pretty much time trying to resolve this issue. Image_Loaded isn't called for the second time, so i thought it's something with item caching. I have tried to set CacheMode to null, but that didn't help. Tried to handle various events but with no success either.
Please help!
Thanks
Download my project - i have removed anything not related to the problem, there are only 90 lines of code.
PS i have found the right event to subscribe, it's Image_DataContextChanges. It seems GridView items are reused, and on updates objects and particular grid items can be confused. Image_Loaded isn't called, so an object gets into a random grid item with arbitrary stretching. DataContextChanges fires each time instead so it can be used to change stretching method on the fly.
And while it works i think the Clemens' solution below is just better. Will use it next time.

Instead of adjusting the Image's Stretch property in code behind, you can put the Image control in a Viewbox, which in addition to Stretch also has a StretchDirection property. If you set that to DownOnly, the images will only be stretched to smaller sizes.
<DataTemplate x:DataType="local:Thing">
<Border BorderBrush="Black" BorderThickness="1" Background="Black">
<Viewbox Width="48" Height="48" Stretch="Uniform" StretchDirection="DownOnly">
<Image Stretch="None" Source="{x:Bind Image}" />
</Viewbox>
</Border>
</DataTemplate>

I fixed your code by making sure that you clean up the Stuff collection entirely when hiding the grid
Stuff = null
This means that in the Show_Click handler, you reinitialize the collection.
Stuff = new ObservableCollection<Thing>();
If you will continue to use bindings, you'll need to raise a PropertyChanged notification when you do that (recommended). If you don't want to use bindings, just re-set the ItemsSource to the new instance of Stuff (see below).
private void Show_Click(object sender, RoutedEventArgs e)
{
Stuff = new ObservableCollection<Thing>();
foreach (var s in source.Except(Stuff)) Stuff.Add(s);
gv.ItemsSource = Stuff;
}
private void Hide_Click(object sender, RoutedEventArgs e)
{
Stuff.Clear();
Stuff = null;
}

Related

UWP XAML ScrollViewer how to show visual indicator of scrollable content

Is there a way to visually indicate that there is scrollable content below the visible area of a <ScrollViewer>?
I see this effect in applications like Microsoft Teams. A shadow appears at the bottom of the scrollable area to indicate that more content is present.
None of the properties of <ScrollViewer> seem to be a clear match. But I'm hoping I can avoid having to programmatically show/hide an element below the <ScrollViewer> based on the scroll position.
I have to say that currently there is no built-in API in ScrollViewer that could directly show a shadow there if the content is not ended.
You might still need to check it programmatically by handling the ViewChanged event of the ScrollViewer and add a custom element.
You could use the following code to check if the ScrollViewer reaches the end:
<Grid>
<ScrollViewer Name="MyScrollViewer" ViewChanged="MyScrollViewer_ViewChanged">
<Rectangle
x:Name="MyRectangle"
Width="3000"
Height="3000"
Fill="Blue"
Margin="20" />
</ScrollViewer>
</Grid>
Code behind:
private void MyScrollViewer_ViewChanged(object sender, ScrollViewerViewChangedEventArgs e)
{
var verticalOffset = MyScrollViewer.VerticalOffset;
var maxVerticalOffset = MyScrollViewer.ScrollableHeight;
if (maxVerticalOffset < 0 || verticalOffset == maxVerticalOffset)
{
// content reaches the end
MyRectangle.Fill = new SolidColorBrush(Colors.Red);
}
else
{
// content does not reach the end
MyRectangle.Fill = new SolidColorBrush(Colors.Blue);
}
}

How to set inktoolbar's pen color

As the code shown, I add a ballpointpen, and it support 30 colors, but not enough.
I got colorSelected(Color type) using some other ways, not discuss here.
Now I want to click ballpointPen, using my colorSelected to draw.
How? Thanks.
<Grid>
<InkToolbar TargetInkCanvas="{x:Bind inkCanvas}" InitialControls="AllExceptPens" VerticalAlignment="Top">
<InkToolbarBallpointPenButton x:Name="ballpointPen" Click="xxx_Click"/>
<InkToolbarCustomToolButton x:Name="toolButtonColorPicker" Click="ToolButton_ColorPicker">
<Image Height="20" Width="20" Source="ms-appx:///Assets/Palette.png"/>
<ToolTipService.ToolTip>
<ToolTip Content="ColorPicker"/>
</ToolTipService.ToolTip>
</InkToolbarCustomToolButton>
</InkToolbar>
<InkCanvas x:Name="inkCanvas" Margin="0,48,0,0"/>
</Grid>
The code below seems not working...
private void xxx_Click(object sender, RoutedEventArgs e)
{
if(bUserDefinedColor)
{
InkDrawingAttributes drawingAttributes = inkCanvas.InkPresenter.CopyDefaultDrawingAttributes();
drawingAttributes.Color = colorSelected;
inkCanvas.InkPresenter.UpdateDefaultDrawingAttributes(drawingAttributes);
}
}
by the way, I upload the test project to GitHub https://github.com/hupo376787/Test.git
Here is a better solution to your problem, without the need of calling UpdateDefaultDrawingAttributes directly.
What I would do is, whenever the user selects a new color from your ColorPicker and hits OK, add this color to the Palette of the InkToolbarBallpointPenButton, and then set the SelectedBrushIndex to the index of the newly created color.
In way you can completely remove your xxx_Click handler, and replace what's in LeftClick with the following
cpx.LeftClick += (ss, ee) =>
{
bUserDefinedColor = true;
colorSelected = cpx.pickerColor;
ballpointPen.Palette.Add(new SolidColorBrush(colorSelected));
ballpointPen.SelectedBrushIndex = ballpointPen.Palette.Count - 1;
};
This is it! You will see the selected color visual on the pen icon automatically reflects the new color, which gives a great user experience.
Here are two more things you might want to do to further enhance the UX.
Cache the added colors and manually add them back to the Palette at app startup so next time when the user opens the app, they are still available.
Instead of adding another icon to display the ColorPicker, try putting it inside the color popup of the InkToolbarBallpointPenButton so all color related things are in the same place. The control that sits inside this popup is called InkToolbarPenConfigurationControl. You should be able to locate its style (see path below) and add your ColorPicker to it.
C:\Program Files (x86)\Windows
Kits\10\DesignTime\CommonConfiguration\Neutral\UAP\10.0.xxxxx.0\Generic\generic.xaml
Hope this helps!

uwp image binding not working with x:Bind

I am trying to bind an imageSource within an ellipse on my XAML page to an ImageSource property in my ViewModel, as I am using MVVM approach in my project. I can confirm by breakpoints that the property in c# gets the image and gets filled, but for some odd reason it doesn't show up in XAML, and when I analyze source property with livepropertyExplorer in the blend, the source in ImageSource shows "0". Here is my code.
XAML
<Ellipse x:Name="ProfileImage" Style="{StaticResource ProfilePicStyle}">
<Ellipse.Fill>
<ImageBrush ImageSource="{x:Bind ViewModel.Banner, Mode=OneWay}"
Stretch="UniformToFill"/>
</Ellipse.Fill>
</Ellipse>
ViewModel
public AllVideosViewModel() : base()
{
//var bit = new BitmapImage();
//bit.UriSource = (new Uri("ms-appx:///Assets/Storelogo.png"));
//Banner = bit;
//Above 3 lines were test code and it works perfect and binding works in this case, but I want the binding as coded in the Initialize method below, because I want that image to bind with the thumbnails of the collection.
Initialize();
}
private async void Initialize()
{
var VideoFiles = (await KnownFolders.VideosLibrary.GetFilesAsync(CommonFileQuery.OrderByName)) as IEnumerable<StorageFile>;
foreach (var file in VideoFiles)
{
<...some code to fill grid view on the same XAML page which works perfect...>
Banner = await FileHelper.GetDisplay(file);//this method returns Image just fine because I am using the same method to display thumbnails on the same XAML page which work fine.
}
}
Thanks in advance.
UPDATE Sample project
sample project to test on github
A few things I have noticed in your code.
Your MainViewModel needs to implement INotifyPropertyChanged and your Banner property needs to raise the property changed event. This is why you don't see any image displayed on the UI.
You should use await bit.SetSourceAsync() instead of bit.SetSourceAsync() as it doesn't block the UI thread.
Since you are not using the Image control but the ImageBrush directly, you should set the decode size to how much you need so you don't waste memory there. Note the Image control does this for you automatically.
bit = new BitmapImage
{
DecodePixelWidth = 48,
DecodePixelHeight = 48
};
Hope this helps!

Change children of StackPanel when StackPanel parent is resizing

I have some StackPanels in a Grid. They are filled with Labels (height of all labels > displayable space). A possible XAML would be:
<Grid>
<StackPanel>
<Label Content="bla" Background="lime" />
<Label ...>
...
</StackPanel>
</Grid>
Every time the size of the Grid changes, I need to change the inner content of the StackPanel. I need to hide the overflowing Label that is only shown partly. To achieve this, I can use following solutions: with a Converter and make a new class that is inherited from StackPanel.
I want to create a different way by using an attached property. I have following code:
//DepProp OverflowVisibility (double), can save height value
public static void Initialized(DependencyObject pObject, DependencyPropertyChangedEventArgs e)
{
var panel = (pObject as Panel) //the StackPanel in this case
panel.SizeChanged += panel_updateInnerLayout;
}
static void panel_LayoutUpdated(object sender, EventArgs e)
{
var parent = sender as Panel;
if(parent != null)
foreach(FrameworkElement element in parent.Children)
{
var elementPos = element.TransformToAncestor(parent).Transform(new Point(0,0));
if(element.ActualHeight + elementPos.Y >
(double)parent.GetValue(OverflowVisibilityProperty))
element.Visibility = Visibility.Hidden;
else
element.Visibility = Visibility.Visible;
}
}
And an example for usage in XAML:
<Grid>
<ItemsControl>
<ItemsPanel>
<ItemsPanelTemplate>
<StackPanel own:OverflowVisibility.OverflowVisibility="{Binding Grid height}" />
</ItemsPanelTemplate>
</ItemsPanel>
</ItemsControl>
</Grid>
Every time the StackPanel changes it's size, I can update my labels with the panel_updateInnerLayout event handler. When the size of StackPanel is changed, everything is working fine.
My Problem: The StackPanel itself doesn't raise SizeChanged because it has a bigger height than the Grid. I need an event that raises every time the Grid changes its height.
My Question: Is there any event instead of SizeChanged, that is called every time I change the Grid size? If not, is there an alternative way with an attached property to solve my problem?
I also tried to set a binding of the height of the StackPanel to ItemsControl ActualHeight but it does not raise the SizeChanged.
Unfortunately, you have chosen the wrong Panel control for your requirements. The StackPanel should only be used when the resizing of its contents is not required, because it doesn't provide any child control resizing abilities. Therefore, you should use a Grid, or any other Panel that provides resizing abilities. Please refer to the Panels Overview page on MSDN for further help with choosing the appropriate Panel.
There are 2 possible events: SizeChanged and LayoutUpdated. SizeChanged doesn't work because the StackPanel is not resized. LayoutUpdated does not work, because the sender of this event is always null and i can not locate what StackPanel was the source.
For more see: Layout Events - SizeChanged and LayoutUpdated.
So there are no possible event available for my approach.
While I searched for a solution, I also found out, that I used the term attached property also for attached behavior and blend behavior. The difference is described here. Summary: properties do nothing with the object but their presence can be used, behaviours change the object behavior. Blend behaviors are behaviors that Microsoft created for Microsoft Blend. The code in my question is attached behavior.
For attached behaviors I didn't found a solution, but for Blend behaviors i found some. You need to add a reference Micosoft.Windows.Interactivity.
After that i used this code:
public class OverflowVisibilityBehavior : Behavior<VirtualizingStackPanel>
{
protected override void OnAttached()
{
AssociatedObject.LayoutUpdated += AssociatedObject_LayoutUpdated;
base.OnAttached();
}
protected override void OnDetaching()
{
AssociatedObject.LayoutUpdated -= AssociatedObject_LayoutUpdated;
}
void AssociatedObject_LayoutUpdated(object sender, EventArgs e)
{
var parent = AssociatedObject; //that solves the problem: you can get a "sender" information
//...
// Instead of property GetValue
(VisualTreeHelper.GetParent(parent) as ItemsPresenter).ActualHeight
}
}
Some problems left: The parent of the StackPanel must raise SizeChanged. If you put a StackPanel in a StackPanel in a Grid, you need modify it.
Second: LayoutUpdated changes the StackPanel several times. More times than i need.

Windows Phone 8.1 Loading Images from Remote URLs Placeholder

I'm messing around with some windows phone development as I'm new to it coming from an Android background.
I am using "theCatAPI" to load a random picture of a cat and show it, then when the picture is clicked, or the button at the bottom of the screen, the image refreshes to a new one.
I have the following so far:
XAML:
<Page
x:Class="CatFactsPics.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:CatFactsPics"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid x:Name="LayoutRoot">
<Grid.ChildrenTransitions>
<TransitionCollection>
<EntranceThemeTransition/>
</TransitionCollection>
</Grid.ChildrenTransitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- TitlePanel -->
<StackPanel Grid.Row="0" Margin="24,17,0,28">
<TextBlock Text="My Application" Style="{ThemeResource TitleTextBlockStyle}" Typography.Capitals="SmallCaps"/>
<TextBlock Text="page title" Margin="0,12,0,0" Style="{ThemeResource HeaderTextBlockStyle}"/>
</StackPanel>
<!--TODO: Content should be placed within the following grid-->
<Grid Grid.Row="1" x:Name="ContentRoot">
<Image HorizontalAlignment="Center" Stretch="UniformToFill" VerticalAlignment="Center" x:Name="KittyPic" Tapped="KittyPic_Tapped"/>
<Button HorizontalAlignment="Center" VerticalAlignment="Bottom" x:Name="newPic" Click="newPic_Click" >New Kitty</Button>
</Grid>
</Grid>
</Page>
and in the page.cs:
...
protected override void OnNavigatedTo(NavigationEventArgs e)
{
this.navigationHelper.OnNavigatedTo(e);
Uri myUri = new Uri("http://thecatapi.com/api/images/get?format=src&type=jpg", UriKind.Absolute);
KittyPic.Source = new BitmapImage(myUri);
}
...
private void newPic_Click(object sender, RoutedEventArgs e)
{
Uri myUri = new Uri("http://thecatapi.com/api/images/get?format=src&type=jpg", UriKind.Absolute);
BitmapImage bmi = new BitmapImage();
bmi.CreateOptions = BitmapCreateOptions.IgnoreImageCache;
bmi.UriSource = myUri;
KittyPic.Source = bmi;
}
I have a couple of questions:
1) is this the correct way of doing things? In Android I'd try and do things asynchronously to avoid stalling the UI thread. That being said, I don't appear to be having any issues with things the way they are now. I'm not familiar with the Windows way of doing things, and haven't found any resources giving any explanation or advice on doing so.
2) There is a delay in displaying the new picture causing a short (couple of second) period where the image view turns black, before the new image reappears. Is there a way of setting it up so either the old picture remains until the new one is physically ready to be displayed, or alternatively display a placeholder "loading" image until the new one can replace it.
Any other advice or tips on how to do things would be great, thanks.
1) With your current code you do not block the UI thread as yes you are setting the URI on the UI thread, but the actually loading of the image is done on another thread automatically. (For doing manually downloading of images, strings etc, you will probably use async/await to avoid locking the UI thread).
2) The image goes black because you change the ImageSource before the new image has loaded. There are as you mention several ways to deal with this. Common for most of them though is that you will want to use the ImageOpened and ImageFailed events on the Image control, which triggers whenever the image is done loading (or an error occurred, for example no internet connection). Here is an example of displaying a loading bar while it is loading, which just hides/shows the loading progress:
In the page.xaml file:
<Grid Grid.Row="1" x:Name="ContentRoot">
<Image x:Name="KittyPic" Tapped="KittyPic_Tapped" ImageOpened="KittyPic_ImageOpened" ImageFailed="KittyPic_ImageFailed" />
<StackPanel x:Name="LoadingPanel" VerticalAlignment="Center">
<ProgressBar IsIndeterminate="True" IsEnabled="True" />
<TextBlock Text="Loading image..." HorizontalAlignment="Center" />
</StackPanel>
</Grid>
And in the page.xaml.cs file
private void KittyPic_Tapped(object sender, TappedRoutedEventArgs e)
{
LoadingPanel.Visibility = Visibility.Visible;
Uri myUri = new Uri("http://thecatapi.com/api/images/get?format=src&type=jpg", UriKind.Absolute);
BitmapImage bmi = new BitmapImage();
bmi.CreateOptions = BitmapCreateOptions.IgnoreImageCache;
bmi.UriSource = myUri;
KittyPic.Source = bmi;
}
private void KittyPic_ImageOpened(object sender, RoutedEventArgs e)
{
LoadingPanel.Visibility = Visibility.Collapsed;
}
private async void KittyPic_ImageFailed(object sender, ExceptionRoutedEventArgs e)
{
LoadingPanel.Visibility = Visibility.Collapsed;
await new MessageDialog("Failed to load the image").ShowAsync();
}
Instead of the TextBlock and ProgressBar you could of course show whatever you want, or for example swapping between two images to keep showing the old one.
For other advice I think when you got used to the basics is to take a look at data bindings which is very helpful and powerful. Also take a look at this article about MVVM pattern.
1) is this the correct way of doing things?
No. It's not. You should definitely adopt the MVVM pattern and detach your business logic from your view - meaning you shouldn't create bitmap images or request/assign such remote image URL's in your view's code-behind. You should be doing such stuff in your ViewModel and bind them to your View.
So in your case there will be an Uri (or a string where you will assign the remote URL) property in your ViewModel (which implements INotifyPropertyChanged) then in your View you will be binding it like this:
<!--TODO: Content should be placed within the following grid-->
<Grid Grid.Row="1" x:Name="ContentRoot">
<BitmapImage UriSource="{Binding BackGroundImage}" CreateOptions="BackgroundCreation" />
<Button HorizontalAlignment="Center" VerticalAlignment="Bottom" x:Name="newPic" Click="newPic_Click" >New Kitty</Button>
</Grid>
Whenever you set the BackGroundImage property you will be raising an event called;
RaisePropertyChanged("BackGroundImage") -> This is the classical MVVM approach.
So that your view will be aware of the fact that the BackGroundImage is changed and it will load it automatically. (But note that if you just provide a string for this BackGroundImage - you will have to use a converter, a string to Uri converter, since it only accepts Uri's for remote images)
2) "...Is there a way of setting it up so either the old picture remains until the new one is physically ready to be displayed, or alternatively display a placeholder "loading" image until the new one can replace it."
I suggest going with displaying a 'loading' image. I experienced the exact same problem as you do here and my workaround for this was inserting a loading image and setting it's opacity value to 0.1 - along with the actual image. While you are switching between remote URL's, when the previous image disappears the opaque loading image appears and when the next actual image is loaded the loading image is not displayed because new image is overwriting it.

Categories

Resources