locked
Best way to set image source on every page RRS feed

  • Question

  • User385023 posted

    What is the best way to set the app background programatically on every page?

    Currently when my app loads, it retrieves an image from the filesystem and whenever a new page is loaded, I pass the image to the next page through its constructor and manually set the image source on that page to be this image. Example:

    (First page)

    public partial class Login : PageParent
    {
        private Image appBackground = new Image();
    
        ...
    
        private async Task LoginExistsAsync()
        {
            appBackgroundBytes = await FileManager.LoadImage(null, "appBackground");
            appBackground.Source = ImageSource.FromStream(() => new System.IO.MemoryStream(appBackgroundBytes));
            GoToMainPage();
        }
    
        void GoToMainPage()
        {
            Navigation.PushAsync(new MainPage(appBackground));
            Navigation.RemovePage(this);
        }
    }
    

    (Second Page)

        public MainPage(Image appBackground)
        {
            InitializeComponent();
            this.appBackground = appBackground;
            background_image.Source = appBackground.Source;
        }
    

    Where background_image is the x:Name of my XML image, which is placed and constrained the same way a background image would be.

    The implications of this method are that on every page the image loads slightly later - which looks tacky. There must be a better way of doing this... I have tried implementing control templates, with the hope that I can define it at the start of the app and have it persist until the app is closed, but the control template is re-defined on every instance call, so the same issue would occur.

    Friday, May 3, 2019 11:29 AM

Answers

  • User385023 posted

    I got it! It turns out it was just a rookie error in the end - I wasn't setting the BindingContext property in my XAML.cs...

    So going forward: I'm setting the background on the first page, assigning it to a variable declared in App.xaml.cs, and using bindings to display it on every page (probably through a control template).

    Although @nick5454 I'm not sure converting the image source from byte[] every time it's rendered is efficient..?

    • Marked as answer by Anonymous Thursday, June 3, 2021 12:00 AM
    Friday, May 3, 2019 3:31 PM

All replies

  • User180523 posted

    Maybe use a ControlTemplate for your pages - setting the image and other shared values in the template.

    Friday, May 3, 2019 12:02 PM
  • User385023 posted

    @ClintStLaurent

    I have tried implementing control templates

    I'm afraid I've already tried this, but there are two problems: 1. It is difficult (I have been unsuccessful so far) to set the image source in a control template from the view model as I am not able to access the image view (the view model can only access XML variables in the view, not variables in the view that have been inherited form the control template, AFAIK) 2. Assuming I was able to set the image source of an item in the control template from the view model, would it persist when using this control template in other pages? Or would I have to manually assign the image source on every page that I use the control template? Because then I would be doing the same thing as I am currently

    Friday, May 3, 2019 12:13 PM
  • User384772 posted

    `public partial class PageParent : ContentPage { private static Image appBackground = new Image(); public Image AppBackground => appBackground;

    private async Task LoginExistsAsync()
    {
        appBackgroundBytes = await FileManager.LoadImage(null, "appBackground");
        AppBackground.Source = ImageSource.FromStream(() => new System.IO.MemoryStream(appBackgroundBytes));
        GoToMainPage();
    }
    
    void GoToMainPage()
    {
        Navigation.PushAsync(new MainPage());
        Navigation.RemovePage(this);
    }
    

    }`

    This should do what your after, however it would be alot nicer if it had been using MVVM.

    Any actions done on AppBackground will be executed on the backing static field of the PageParent class.

    Friday, May 3, 2019 1:02 PM
  • User180523 posted

    Uh..... I have concerns about how you're doing things and conceptual understandings, based on your reply.

    as I am not able to access the image view (the view model can only access XML variables in the view, not variables in the view 1. ViewModels aren't supposed to be accessing UI elements. ViewModels are ignorant of the 0:n views that might be using them as binding context. How could your ViewModel know which of 20 different views to affect? You do realize that Views and ViewModels are not a one-to-one relationship, right? 2. "XML Variables". Huh? First of all, UI isn't built in XML, its built in XAML. Next what "variables" are you talking about? XAML isn't executable: There aren't 'variables' in XAML

    Maybe I misunderstood something so let me double check. Your original description sounded like you were wanting the same image on every page. Is that the case? If so, you just define it one time in the ControlTemplate, and use that template for every page. Easier still... Just set the Wallpaper of your app to that one image and be done with it.

    If not... If you're saying you want a different background of each page, then just bind the background image source to a property on the ViewModel - same as you would do with any other property such as color or text.

    Friday, May 3, 2019 1:10 PM
  • User385023 posted

    @DJNova That's cool, but once the Image is accessible through PageParent I still have to manually set it on every page right? Or was your intention that I would set the image binding to be the PageParent AppBackground.source? Because if I try creating the new variable in PageParent: public ImageSource AppBackgroundSource => appBackground.Source;

    And then using an Image source binding in the XML: <Image Source="{Binding AppBackgroundSource}" x:Name="background_image" AbsoluteLayout.LayoutBounds="0,0,1,1" AbsoluteLayout.LayoutFlags="All" Aspect="AspectFill" />

    This doesn't seem to work.. Is that because the AppBackgroundSource in PageParent is not accessible until the current page has fully initialised?

    @ClintStLaurent Sorry for not being clear. It will be the same image on every page, but this image is retrieved on the first page of the app (therefore can change across app instances). Therefore (as far as I know) I can't define it in a control template, because at the time of creation of the control template it does not know the file path of the image (because it can vary across platforms). Regarding wallpaper - isn't that Android specific?

    just bind the background image source to a property on the ViewModel

    Isn't that what I'm currently doing on every page? With: background_image.Source = appBackground.Source;

    Or will using a source binding allow the page to be rendered with the image source set, instead of the page rendering and me setting the image source after?

    Friday, May 3, 2019 1:27 PM
  • User72117 posted

    @Chris_xam Whoa, you're loading the image on every page? Store that image as a byte[] somewhere like

    public static byte[] MyGoofyImage {get; set; }

    Then reference that

    backgeround_image.Source = App.MyGoofyImage

    of course you'll need to convert that if you're using MVVM

    otherwise you can do Source = new MemoryStream(App.MyGoofyImage)

    I think you should re think how you're getting that image and set it for a profile and save it somewhere. Sounds like you're loading it from the API every which is way too chatty

    Friday, May 3, 2019 1:51 PM
  • User180523 posted

    @Chris_xam said: Sorry for not being clear. It will be the same image on every page, but this image is retrieved on the first page of the app

    Here's another concept issue. Work doesn't happen on pages. Well, its not supposed to. Pages are just UI. They are a reflection of data in your App or ViewModel. They are a way to interact with data. That's it. You're not supposed to be doing work such as retrieving data from a page.

    If its going to be the same image for the lifespan of the app, that's great. Retreive it at the App level when you launch and keep it as an App scoped property, then bind to it. You can either put it in the markup of all your pages, or put it in the ControlTemplate used by all your pages.

    (therefore can change across app instances). "across app instances"... Hum? Do you mean this app will run three instances on the same device at the same time? Or do you mean you're using the same code to pretend to be 3 different apps each with a different background?

    Isn't that what I'm currently doing on every page? With: background_image.Source = appBackground.Source; I don't think so. Didn't you say you were pushing the image from page to page as a dependency in the constructor? Navigation.PushAsync(new MainPage(appBackground)); That's a real mess and no reason to do it.

    Sidenote void GoToMainPage() { Navigation.PushAsync(new MainPage()); Navigation.RemovePage(this); } Why are you doing navigation like this? Why a new MainPage ever time? Almost always that MainPage acts like a home page for most apps. Just make it once and leave it in place. Then you navigate to however many pages. Then youBack when you are done; traveling backward in the navigation stack to pages that already exist and are already rendered. Its a LOT faster than rendering the home page over and over and over from scratch. If you want to make a big jump back... say you've navigated 5 pages forward and are done and want to just jump back to home: Then loop the back navigation until you are home.

    Friday, May 3, 2019 1:51 PM
  • User72117 posted

    I use this for my biding Source="{Binding MyGoofyImage, Mode=OneWay, Converter={StaticResource ISConverter}}"

    The ISCOnverter simple coverts from a byte[] to a stream

    `public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { ImageSource retSource = null;

                if (value != null)
                {
                    byte[] imageAsBytes = (byte[])value;
                    retSource = ImageSource.FromStream(() => new MemoryStream(imageAsBytes));
                }
                return retSource;
    
        }`
    
    Friday, May 3, 2019 1:53 PM
  • User385023 posted

    @nick5454 said: @Chris_xam Whoa, you're loading the image on every page? Store that image as a byte[] somewhere like

    public static byte[] MyGoofyImage {get; set; }

    Then reference that

    backgeround_image.Source = App.MyGoofyImage

    of course you'll need to convert that if you're using MVVM

    otherwise you can do Source = new MemoryStream(App.MyGoofyImage)

    I think you should re think how you're getting that image and set it for a profile and save it somewhere. Sounds like you're loading it from the API every which is way too chatty

    Thanks - that's a better idea. So I load the image, store it in App.MyGoofyImage, but when I set the <Image source="{Binding MyGoofyImage}" .../> on the next page, this doesn't seem to work. Is there any reference I have to make in the XAML to allow App.MyGoofyImage to be accessible? E.g a reference to the App namespace or something? And does the binding have to be of type ImageSource or can it be of type Image?

    Friday, May 3, 2019 2:14 PM
  • User385023 posted

    @ClintStLaurent Thanks for your advice - I'll fix that navigation issue. As for the background image problem - I agree storing the image as an App scoped property is a much better solution, but I'm having real trouble getting that Image source binding working (as I explain in my comment above)

    Friday, May 3, 2019 2:17 PM
  • User72117 posted

    @Chris_xam said:

    @nick5454 said: @Chris_xam Whoa, you're loading the image on every page? Store that image as a byte[] somewhere like

    public static byte[] MyGoofyImage {get; set; }

    Then reference that

    backgeround_image.Source = App.MyGoofyImage

    of course you'll need to convert that if you're using MVVM

    otherwise you can do Source = new MemoryStream(App.MyGoofyImage)

    I think you should re think how you're getting that image and set it for a profile and save it somewhere. Sounds like you're loading it from the API every which is way too chatty

    Thanks - that's a better idea. So I load the image, store it in App.MyGoofyImage, but when I set the <Image source="{Binding MyGoofyImage}" .../> on the next page, this doesn't seem to work. Is there any reference I have to make in the XAML to allow App.MyGoofyImage to be accessible? E.g a reference to the App namespace or something? And does the binding have to be of type ImageSource or can it be of type Image?

    @Chris_xam Not the greatest idea, but in your App.xaml.cs file add that property and you're done or any class. It's a static property.

    https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/static

    Friday, May 3, 2019 2:20 PM
  • User385023 posted

    @nick5454 @ClintStLaurent

    Not the greatest idea, but in your App.xaml.cs file add that property and you're done or any class. It's a static property.

    Thanks for your help -

    So I've defined MyImageSource in App.xaml.cs: public partial class App : Application { public static ImageSource MyImageSource { get; set; }

    Assign it on the first page: App.MyImageSource = ImageSource.FromStream(() => new System.IO.MemoryStream(appBackgroundBytes));

    Then in the markup on the next page, reference this in the binding: <Image Source="{Binding App.MyImageSource}" x:Name="background_image" AbsoluteLayout.LayoutBounds="0,0,1,1" AbsoluteLayout.LayoutFlags="All" Aspect="AspectFill" />

    But the image still does not show. What am I doing wrong? I also tried:

    <Image Source="{Binding MyImageSource}" x:Name="background_image" AbsoluteLayout.LayoutBounds="0,0,1,1" AbsoluteLayout.LayoutFlags="All" Aspect="AspectFill" />

    Friday, May 3, 2019 2:31 PM
  • User72117 posted

    @Chris_xam you're going to need a converter is you're using MVVM. It won't work using an actual ImageSource.

    Take the Converter code in my previous reply create a class "ISConverter" then in your page paste my binding in it. The Intellisence will ask you to add a reference

    Then for your view model. Do you have INotifyPropertyChanged? If so add this property

    public byte[] MyMainImage {get => App.MyGoofyImage}

    change you binding to by my MainImage instead and that will work. Ypu'll have to play around with it a little bit.

    watch this short video https://youtube.com/watch?v=GZDQptTQZsk

    Friday, May 3, 2019 2:38 PM
  • User385023 posted

    @nick5454

    Thanks, I'll play around and let you know.

    Friday, May 3, 2019 2:54 PM
  • User385023 posted

    I got it! It turns out it was just a rookie error in the end - I wasn't setting the BindingContext property in my XAML.cs...

    So going forward: I'm setting the background on the first page, assigning it to a variable declared in App.xaml.cs, and using bindings to display it on every page (probably through a control template).

    Although @nick5454 I'm not sure converting the image source from byte[] every time it's rendered is efficient..?

    • Marked as answer by Anonymous Thursday, June 3, 2021 12:00 AM
    Friday, May 3, 2019 3:31 PM
  • User72117 posted

    @Chris_xam it's completely up to you. I personally prefer to store primitives instead of objects. This allows me to create a converter for whatever I need and not get into the old "OMG it's an Image class, but I need a Whatever Layout class now! I have to write a custom conversion. Ugg". Whereas, if it's a byte array, int, string you can easily format it to whatever you need. Besides that image is probably a byte[] in your POCO

    an Image is a fancy byte[] in my opinion and it's very cheap to create a stream wrapped around a byte[]

    In the end, whatever you're comfortable with

    Friday, May 3, 2019 3:42 PM