C#: Dynamically  adding to a flowlayoutpanel

trevor1940
trevor1940 used Ask the Experts™
on
Hi
I'm trying to add items to a flowlayoutpanel  something like the picture

kate.JPG
I've managed a picture but the hover is failing because BigPicturePath is being overwritten
so
1. How do I use the picture.tag to pass the url into ThumbPictureBox_MouseHover method
2. How Do I adjust the linq query to order on movies.movielinkpersons.role

3. In order to create the picture example I'm guessing I'd need to create a 2 X 2 TableLayoutPanel with Col1 rowspan of 2 adding a Picture box to col1 a label to col2 row1 (with a click function) and a button to Col2 Row2

What concerns me is my form has a 3 column TableLayoutPanel with a second TableLayoutPanel  in the center column with the flowlayoutpanel   in 1 cell show here

FilmForm.JPG
In the code bellow GetCast is triggered on clicking a title  in a listview

        private void GetCast(int id)
        {
            using (var context = new MoviesEntities())
            {
                var Movies =  context.movielinkpersons.Where(p => p.movieId == id).ToList();
                int i = 1;
                foreach(var person in Movies)
                {
                    string Thumb = "";
                    if (person.person.profile_path == "/JohnWayneBig.png")
                    {
                        Thumb = ImgPath + "JohnWayneSml.png";
                        BigPicturePath = ImgPath + "JohnWayneBig.png";

                    }
                    else
                    {
                        Thumb = ImgURL + "w45" + person.person.profile_path;
                        BigPicturePath = ImgURL + "original" + person.person.profile_path;
                    }
                    
                    var picture = new PictureBox
                    {
                        Name = "pictureBox" + i.ToString(),
                        Size = new Size(45, 70),
                        

                    };
                    picture.Tag = BigPicturePath;
                    picture.MouseHover += ThumbPictureBox_MouseHover;
                    picture.LoadAsync(Thumb);
                    flowLayoutPanel1.Controls.Add(picture);
                    i++;
                }


            }
        }

    private void ThumbPictureBox_MouseHover(object sender, EventArgs e)
    {
            //BigPicturePath = this.Tag.ToString();
        if (BigPicturePath != null)
        {
            string Orientation = "P";
            FilmsDB.PictureForm frm = new PictureForm(Orientation);
            frm.SetValues(BigPicturePath);
            frm.ShowDialog();
        }

    }

Open in new window

Comment
Watch Question

Do more with

Expert Office
EXPERT OFFICE® is a registered trademark of EXPERTS EXCHANGE®
Commented:
There's a few different questions here, but I'll try to give some advice on each one.


First off, this isn't specific to your question, but you're still using Windows Forms, which is basically in its final stages of life. Five years ago, Microsoft said that they're not going to improve Winforms anymore - that it's in maintenance mode, which means the only changes it will get are bug fixes. In the meantime, WPF is the new way to do this. It has broader overall compatibility, looks better, and has a lot of UI-specific abilities that Winforms does not. For a long time, I was so used to Winforms that I resisted learning WPF, but after using it for a while now, it's almost ridiculous how easily I can throw together a nice-looking UI without doing the hundred little details that my Winforms projects required (all the little UI bugs like strangely-behaving tooltips, dynamic sizing, etc).

So I'd strongly recommend that you start learning and playing with WPF. It's perfect for the kind of UI you're showing.

That said, I'm going to address things using the Winforms mentality that you're already using.

1. What you have here is a perfect use case for a custom UserControl. Currently, it looks like you're just dealing with individual PictureBox controls, which you can probably get away with, but it won't allow you much breathing room. As a general rule, the moment you start trying to use the Tag property to store some data, you should start considering the use of a custom UserControl that has its own properties, like this (pseudo-code):

public class ActorBox : Panel
{
  public ActorObject Actor { get; set; }
  private PictureBox pbPhoto { get; set; }
  public string ThumbPhotoPath { get; set; }
  public string FullPhotoPath { get; set; }

  public ActorBox(ActorObject actor)
  {
    Actor = actor;
    ThumbPhotoPath = "...";
    FullPhotoPath = "...";

    pbPhoto = new PictureBox() { ... };
    pbPhoto.MouseHover += ThumbPictureBox_MouseHover
    this.Controls.Add(pbPhoto);
    pbPhoto.LoadAsync(ThumbPhotoPath);
  }

  private void ThumbPictureBox_MouseHover(object sender, EventArgs e)
  {
        if (FullPhotoPath != null)
        {
            string Orientation = "P";
            FilmsDB.PictureForm frm = new PictureForm(Orientation);
            frm.SetValues(FullPhotoPath);
            frm.ShowDialog();
        }
    }
}

Open in new window


...and in your parent form loop:
foreach(var person in Movies)
                {
                    var actorBox = new ActorBox(person);
                    flowLayoutPanel1.Controls.Add(actorBox);
                }

Open in new window


This way, your ActorBox user control has complete control of its own layout (so it can grow or shrink or change as necessary without impacting any other ActorBox instances), has its own copy of a picture box, its own event handler, and its own properties, so there's no risk of variables getting overwritten. You don't have to keep an "i" counter for naming your picture boxes, or anything like that. And if you need the same kind of functionality elsewhere in your application, it's as easy as simply adding a new ActorBox() instance wherever you want it.

2. If you don't want to use a custom user control, the "quick fix" answer here is to cast the "sender" property to a PictureBox and then cast the Tag property as a string and save that into a separate variable that only exists inside your method, like this:

private void ThumbPictureBox_MouseHover(object sender, EventArgs e)
    {
        string bigPicturePath = ((PictureBox)sender).Tag.ToString();
        if (bigPicturePath != null)
        {
            string Orientation = "P";
            FilmsDB.PictureForm frm = new PictureForm(Orientation);
            frm.SetValues(bigPicturePath);
            frm.ShowDialog();
        }
    }

Open in new window


...but again, that's a quick fix, not a GOOD fix. It's much better in every respect to use a custom user control here.

3. As for sorting, just add an OrderBy:
context.movielinkpersons.Where(p => p.movieId == id).OrderBy(mp => mp.movielinkpersons.role).ToList();

(This presumes that "movielinkpersons" will never be null. If there's any nulls, then you'll get an exception if you try this.)

Author

Commented:
Apologies for delay in responding the Bank holiday weekend got in the way

Like you I've been putting of   playing with WPF trying to learn Entity Framework and LINQ query

I think what I'm going to do is finish current project in Win forms then start again in WPF

Can you reconmend any good tutorials?

Author

Commented:
In the code you posted I'm getting the erros in the pic

I thought I was passing a reference  the database object / var person to the ActorBox do I need to create a ActorObject Class?

UserControl.JPG
Angular Fundamentals

Learn the fundamentals of Angular 2, a JavaScript framework for developing dynamic single page applications.

Commented:
My code was just starter code. I didn't know your class names so you will have to replace the data types appropriately and tweak it to fit your specific code. So change things like ActorObject to movielinkperson for example.

Author

Commented:
On the Main form I now have this!

person is being passed to ActorBox
 

        private void GetCast(int id)
        {
            using (var context = new MoviesEntities())
            {
                var Movies =  context.movielinkpersons.Where(p => p.movieId == id).OrderBy(mp => mp.role) .ToList();
                foreach(var movielinkperson in Movies)
                {
                    var person = movielinkperson.person;
                    

                    var actorBox = new ActorBox(person);
                    flowLayoutPanel1.Controls.Add(actorBox);
                }


            }
        }

Open in new window


I created the ActorBox by Solution Explorer => Add => User Control
this has 1 each of ThumbPictureBox, NameLable & a filter button and set the events

 ActorBox.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using MoviesDataModel;

namespace FilmsDB
{
    public partial class ActorBox : UserControl
    {
        public ActorBox()
        {
        }
        const string ImgPath = @"E:\Media\thumbs\Movies\";
        const string ImgURL = "https://image.tmdb.org/t/p/";
        const string OriginalImgPath = "http://image.tmdb.org/t/p/original";
        private person person;

        private PictureBox pbPhoto { get; set; }
        public string ThumbPhotoPath { get; set; }
        public string FullPhotoPath { get; set; }

        public ActorBox(person person)
        {
            InitializeComponent();
            this.person = person;
            if (person.profile_path == "/JohnWayneBig.png")
            {
                ThumbPhotoPath = ImgPath + "JohnWayneSml.png";
                FullPhotoPath = ImgPath + "JohnWayneBig.png";

            }
            else
            {
                ThumbPhotoPath = ImgURL + "w45" + person.profile_path;
                FullPhotoPath = ImgURL + "original" + person.profile_path;
            }
            

            ThumbPictureBox.LoadAsync(ThumbPhotoPath);
            ActorNameLbl.Text = person.name;
        }

        private void ActorNameLbl_Click(object sender, EventArgs e)
        {
            MessageBox.Show("Not yet {0}", person.tmdb_id.ToString());
        }

        private void FilterBTN_Click(object sender, EventArgs e)
        {
            //Form1.PeopleSearchTxtBox.Text;
        }

        private void ThumbPictureBox_MouseHover(object sender, EventArgs e)
        {
            if (FullPhotoPath != null)
            {
                string Orientation = "P";
                FilmsDB.PictureForm frm = new PictureForm(Orientation);
                frm.SetValues(FullPhotoPath);
                frm.ShowDialog();
            }
        }
    }


}

Open in new window


On clicking the Filter Button how do I pass NameLble.text back to the main form

Commented:
Also, for a WPF tutorial, I just read through the tutorials here:
https://wpf-tutorial.com/

However, there are plenty of others, and I'm writing my own article on getting started with moving from Winforms to WPF.

Commented:
If you want the main form to respond to events that happen within the user control, then you would first add an event in your ActorBox user control, and then call it when you click the button:

namespace FilmsDB
{
    public partial class ActorBox : UserControl
    {
        ...existing code...

        private void FilterBTN_Click(object sender, EventArgs e)
        {
            // STEP 2: Raise your new "OnFilterClick" event for anyone 
            // who might be listening/subscribing to this event, and pass 
            // the current ActorBox (this) as the sender, and 
            // ActorNameLbl.Text as the string.
            OnFilterClick?.Invoke(this, ActorNameLbl.Text);
        }

        // STEP 1: Create a new event that passes a string
        public event EventHandler<string> OnFilterClick;
    }
}

Open in new window


From there, your main form simply has to subscribe to the OnFilterClick of each ActorBox it creates:
        private void GetCast(int id)
        {
            using (var context = new MoviesEntities())
            {
                var Movies =  context.movielinkpersons.Where(p => p.movieId == id).OrderBy(mp => mp.role) .ToList();
                foreach(var movielinkperson in Movies)
                {
                    var person = movielinkperson.person;
                    

                    var actorBox = new ActorBox(person);
                    actorBox.OnFilterClick += ActorBox_OnFilterClick; // <===================
                    flowLayoutPanel1.Controls.Add(actorBox);
                }


            }
        }
       
        ...

        private void ActorBox_OnFilterClick(object sender, string actorName)
        {
            var actorBox = (ActorBox)sender;
           
            /*
            Add whatever code you want into this event handler - you have access to 
            both the actorName string and ALSO the instance of the ActorBox
            where the filter button was clicked.
            */
        }

Open in new window


On a side note, it's usually good practice to not name your properties the same as your data type. So you had this property in your ActorBox:
private person person;

Maybe change your class name/data type to be proper case, like Person instead of person:
private Person person;

You would also have to change the rest of the instances of person, but if you use the Visual Studio renaming functionality (right-click on the data type and choose Rename in the popup), it should change them all for you instantly and it will only change the actual type name (so other instances of "person" will stay "person" while all the data types will change from "person" to "Person").

Check out this page on some really common and useful naming conventions:
https://www.c-sharpcorner.com/UploadFile/8a67c0/C-Sharp-coding-standards-and-naming-conventions/

Once you get used to following these rules/conventions, you'll actually code a little bit faster, too. It reduces some of those "what should I call this?" moments and will set you up for future success, especially when you get into WPF where you sometimes will have two copies of a field/property, like:

private string _myField;
public string MyField
{
  get { return _myField; }
  set
  {
    if(_myField != value)
    {
      _myField = value;
      NotifyPropertyChanged();
    }
  }
}

Open in new window


It probably doesn't make sense why you would do that right now, but it will make sense later on.

Author

Commented:
Thanx for the link to https://wpf-tutorial.com/ I had a look at https://www.wpftutorial.net/Home.html and very quickly got overwhelmed by the volume of information  

Do you have an ETA for finishing your article on getting started with moving from Winforms to WPF?
When you've finished PM a Link I could be your guinea pig

BTW I couldn't access  ActorNameLbl.Text from the main form but I was able to access public string ActorName { get; set; }

Do more with

Expert Office
Submit tech questions to Ask the Experts™ at any time to receive solutions, advice, and new ideas from leading industry professionals.

Start 7-Day Free Trial