Windows Phone Tutorial:ListBox on Windows Phone 7

This is one of the tutorial that walks through development of application on latest Windows Phone 7

Here I will show you how to use ListBox on Windows Phone and how to Bind Data to it.

First of all, you have to insert a ListBox into the PhoneApplicationPage, also I have edit the PageName to Transactions

Apart from dragging the ListBox control from the toolBox into the Application Page, you can also Code it by yourself.

Remember to give it a Name here is “TransactionList” so that we can call it from codebehind directly.

Next, we have to create a class which clarify the structure of a transaction.

Here for simply, all data are in string data format, Date, Amount and Type.

You can see a switch-case here so as to load specific image representing the type of the transaction.

Construct your ListBox

here we construct our listbox with some stackPanel

The following Code will generate a button like the following, it is quite dull with black background and white words.

The latter part of this tutorial will teach your how to design your own style.

Finished our design part, we have to jump to the code part.

It is better we create a loaded event handler to put our code so as to ensure all components are fully loaded.

Then, inside the Loaded event, I have some lines to generate a set of data instead of reading from XML or from the internet.

It is all done for our first Windows Phone 7 Application with ListBox

here is the result

Adding own Style to ListBoxItem

For this I will teach it on the next tutorial.

Thanks

135 thoughts on “Windows Phone Tutorial:ListBox on Windows Phone 7

  1. hey,

    this is really nice, thank you very much.
    but I have 2 questions. Is it possible to implement paging like on aspx?
    space between item borders (between rectangles) how can I reduce it?

    Reply
  2. Pingback: Mas de 30 tutoriales sobre desarrollo en Windows Phone 7 « josemiguel.torres

  3. Pingback: 30+ Excellent Windows Phone 7 Development Tutorials - BooleanBase

  4. Pingback: 30+ Amazing Windows Phone 7 Development Tutorials | Windows space

  5. Pingback: +30 Tutoriales de Windows Phone 7 - Blog de Oskar Alvarez

  6. Pingback: Tutoriales para Windows Phone 7 « Gabriel D. Sulé

  7. Hello Steve,

    I’ve got a question about your example:
    How do you retrieve the Transaction class corresponding to a clicked button?
    I’ve added an on_click event to the button in the xaml file, but I’ve been unable to find a way to the corresponding bounded Transaction object.
    Could you please help me out?

    Thanks in advance!

    Reply
    • Sorry for replying late

      When you bind the List of TransactionClass on the list
      you know the SelectedIndex?
      having both selectedIndex and the original List, you can get the corresponding one.

      Reply
  8. Hello ,

    this is a great tutorial, but i think i miss something in it, my problem is i am not getting the items that are binded to the itemBox. all i get is the number of them but not the content. if anybody knows a solution that would be great

    Thanks,

    Reply
  9. Pingback: Tutoriales de Windows Phone 7 « Gabriel D. Sulé

  10. 3water :
    Sorry for replying late
    When you bind the List of TransactionClass on the list
    you know the SelectedIndex?
    having both selectedIndex and the original List, you can get the corresponding one.

    I’ve already fixed my problem, but thanks for the reply anyway 🙂
    The problem I has was that I didn’t get a selectedIndex in my listbox because the button trigged the on_click event, instead of the listbox item triggering an selection_changed event.
    My fix: I have the button an on_click event in which I was able to retreive the correct object using this:
    Button source = (Button)e.OriginalSource;
    Transaction selectedTransaction = (Transaction)source.DataContext;

    🙂

    Reply
    • you should also be able to get the object by the sender

      ClassName yourObject = sender as ClassName;

      For example, in the event, you will have the …..(object sender, ….Args e)

      simply call Button myButton = sender as Button;
      you will get your button reference 🙂

      Reply
  11. Please tell me how i can fire event based on button click from the listbox.
    And to also read what button was pushed.
    i plan to list names in listbox and I need toi know what name was clicked.

    Reply
  12. crunchycomics :
    Please tell me how i can fire event based on button click from the listbox.
    And to also read what button was pushed.
    i plan to list names in listbox and I need toi know what name was clicked.

    You would set up an event for the button like any other button by assigning it to the Click event ie: >button click=”user_click”<

    In your event handler, you would access the sender’s data context which would be your binding object ie:
    var myUser = ((Button)sender).DataContext as User;

    hope this helps.

    Reply
    • Thank you so much for this specific example, I’ve been searching for over an hour now trying to figure out how to get access to the data item for the row’s uindex.

      Reply
  13. Thanks this was helpful.

    I created a button event handler:
    private void patientslistbn_Click(object sender, RoutedEventArgs e)
    {
    var myButton = ((Button)sender).FindName(“type”);
    MessageBox.Show(“Button Clicked ” + myButton);
    }

    BUT are not getting a value. Please give me a sample of getting values from button and from routedeventsargs (.e).

    Reply
    • for getting the reference of button, you can do like this

      Button myButton = sender as Button;

      for getting the values from button

      you can do it from the reference you get:

      MessageBox.Show(“Button Clicked” + myButton.Content);

      Hope it helps

      Reply
  14. Nice tutorial and has been helpful. I have implemented a version of it and can get the button content from object sender. How can I tell which item index was clicked? For example, let’s say that I have 5 button items in my ListBox and I clicked on the second button from the top of the list. I need to know that the index clicked on was index=1. Can you help in determining the index of the clicked item?

    Reply
    • if you got the sender of the clicked Item, and together having the listbox (let say named myListbox)
      you can do like this

      Button clickedButton = sender as Button;
      int index = myListbox.Items.indexOf(clickedButton);

      Hope it will help you 🙂

      Reply
      • Perfect, this is exactly what I needed. I have verified that I can retrieve the needed index. Cheers! 😀

        Reply
      • I tried doing that and it gives me a compilation error:

        Error 1 ‘System.Windows.Controls.ListBox’ does not contain a definition for ‘indexOf’ and no extension method ‘indexOf’ accepting a first argument of type ‘System.Windows.Controls.ListBox’ could be found (are you missing a using directive or an assembly reference?)

        Reply
        • ListBox dont have the indexOf, but ListBox.Items does have

          try
          int index = ListBox.Items.indexOf(yourlistboxItem);

          Reply
        • Thanks for your comment.
          I have updated my comments made before, it should be ListBox.Items.indexOf(clickedButton)
          Please double check.

          Reply
        • If your button is set press on clickmode, the selectedindex will be -1 . You have to trace back the parent Sent from my Windows Phone

          Reply
          • I’m french and do not understand English well. What do you mean, You Have To trace back the parent Sent from my Windows Phone ?

            Reply
  15. Pingback: My favorite Windows Phone 7 development resources

  16. Excellent tutorial, thanks:))
    I have a small problem
    I want in the click of a button
    I get the value of Amount
    in a string variable?!

    Thank you answer me:)

    Reply
    • Since that is a Listbox you can add the selection changed event to the listbox. It will be fired when users click on any one of the button in the listbox in this tutorial. The you can get the selectedIndex and just refer to the list that you have binded to the listbox then you can get the amount

      Reply
      • no matter what i do, the event “Selection_Changed” on the listbox doesn’t fire – with one exception, and i have no idea why it has only worked that once…
        any ideas on this would be appreciated, thanks in advance

        Reply
    • Private Sub Button_Click(sender As System.Object, e As System.Windows.RoutedEventArgs) Handles button1.Click

      Dim myButton = TryCast(DirectCast(sender, Button).DataContext, Transaction)
      Dim index As Integer = TheList.Items.IndexOf(myButton)

      MessageBox.Show(“clicked” & vbCrLf & index)

      End Sub

      Reply
  17. Excellent example. My first grid up and running in less than thirty minutes. In fact it was faster to follow your example then to search for something useful on MSDN!

    Reply
  18. Pingback: 30+ Excellent Windows Phone 7 Development Tutorials | TechSoc

    • yes:) set the margin of each item. Of course you can make a template and then bind the data to listbox, then every items will follow template’s margin
      On the other hand, you can add all items in XAML, and set them one by one too. Just see which workaround you would like to choose.

      Just like this

      Reply
  19. Pingback: List box windows phone |

  20. Pingback: Статьи для разработчиков WP7 « mudryck

  21. hello,
    I try to retrieve the name of the selected item but I can not.
    Can someone help me?

    Here’s what I do:

    private void Button_clicked (object sender, SelectionChangedEventArgs e)
    {
    Button myButton = sender as Button;
    MessageBox.Show (“Button Clicked” + myButton.Content);
    }

    thank you

    Reply
    • You can simply store data in observableCollection and bind it to the listbox, it will automatically update when you update the collection Sent from my Windows Phone

      Reply
  22. ya i’ve used that but i can’t do the “friendsList.Items.Clear();” . it gave me a error “Operation not supported on read-only collection.”

    Reply
    • If you want to delete all items in collection, simply use observableCollection.Clear(). If you just want to clear the listbox, simply use listbox.itemsSource = null Sent from my Windows Phone

      Reply
  23. Hi. Thank you for the tutorial.
    I am having one problem though. In my application, I want to change each button’s foreground when they are clicked. To do so, I wrote the following event handler:

    private void contactSelected(object sender, RoutedEventArgs e)
    {
    Button b = (Button)sender;
    b.Foreground = new SolidColorBrush(Colors.Cyan);
    }

    Apparently, it works. However, when I scroll down in the list, I see that more buttons (which have not been clicked) have also changed their foreground color. Do you know why this happens? It’s like the reference (Button b) is not unique and the change is applied to more than one item in the list.

    I would appreciate any help. Thank you.

    Reply
  24. hi please help me i’m having an error: “The attachable property ‘ItemTemplate’ was not found in type ‘ListBox’.”

    Reply
    • check your xaml and make sure it matches what’s in the example. If not, post it so others can take a look for you

      Reply
  25. ferl :
    i already fix it.. but i got an error that says ” TransactionList’ is not a valid value for Name”.

    here’s the code.. the ERRor is on line 1

    Reply
  26. Pingback: Mas de 30 tutoriales sobre desarrollo en Windows Phone 7 « desarrolloMobile.NET

  27. Brilliant, but how can i remove the current ListView.Itemsource and replace by the other.
    I did it by reassigning the Itemsource property but on the screen, it did not display the new one. How can i fix it, friend?

    Reply
  28. Great tutorial, thank you.
    But I have a question: How can I add extra items to the listbox at runtime? I have added an object to the List, but nothing changed. I even tried to set the Itemsource again after adding an object, but this didn’t work.

    Reply
  29. thanx for the example it’s worked with me but I have one problem when use textblock in xaml wen i use {Binding Name} it back empty value can i know a reason for this or a soiltion??plz

    Reply
    • Check the object if it has a property called Name case sensitive Also check if you have binded to itemsource

      Sent from my Windows Phone

      Reply
  30. hey i tried to browse thru the responses and comments, but ive been curious if there is a way to import data from the WWW using WP7 apps (I.E. for making weather apps, displaying data about computer specs, etc.)

    Reply
  31. Hi, isit possible to binding image from sql server to listbox?because my data is from sql server. i had tried to put image into it. but the image didnt show up..

    Reply
    • You can get the data from SQL using wcf service in between SQL server and Silverlight. After that you can bind observationlist to the listbox

      Sent from my Windows Phone

      Reply
  32. Pingback: ListBox data binding with List of List containing string string int | Jisku.com - Developers Network

  33. Pingback: ListBox data binding with List of List containing string string int

  34. var appStorage = IsolatedStorageFile.GetUserStoreForApplication();

    List noteList = new List();

    string[] fileList = appStorage.GetFileNames(“*.txt”);
    string[] date = new string[fileList.Length];
    for (int i = 0; i < fileList.Length; i++)
    {
    date[i] = appStorage.GetCreationTime(fileList[i]).ToString();
    }
    for (int i =0; i<fileList.Length;i++)
    {

    noteList.Add(new Note() { fileName = fileList[i], fileDateCreate = date[i] });
    }
    ListBox_Note.ItemsSource = fileList;

    I have st wrong with this code.
    If Listbox_Note.ItemsSource = filelist, I can add and delete file ok. The bindlist will update correct.
    But if = noteList. I can add the new file. But the delete will not disapear. It still threre.If i delete it still return that file not exist // testing for file exist.
    So i don't know I should use noteList.Add or what.

    The class Note have two property
    fileName and fileDateCreate. nothing special

    Reply
    • Found the error. When i delete. I use the seleteditem.tostring()
      but the selected item is class note not string .
      So i use this instead
      -Note fileNote = ListBox_Note.SelectedItem as Note;
      then delete
      -fileNote.fileName
      (fileName is property of the class).
      Thanks wong for help but i found the error.

      Reply
  35. I make index for List.

    Button source = (Button)e.OriginalSource;
    Transaction selected_list_item = (TransactionList)source.DataContext;
    int index = TransactionList.Items.IndexOf(selected_list_item);

    That code is work for me and it generate correct index of list.

    Reply
  36. Pingback: Data for Windows Phone | Dawei's space

  37. // Handle selection changed on ListBox
    private void MainListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
    // If selected index is -1 (no selection) do nothing
    if (Options.SelectedIndex == -1)
    return;

    // Navigate to the new page
    NavigationService.Navigate(new Uri(“/French/fDetails.xaml?selectedItem=” + Options.SelectedIndex, UriKind.Relative));

    // Reset selected index to -1 (no selection)
    Options.SelectedIndex = -1;

    }

    i am trying to display the different contents on the listbox without having to create a different page

    Reply
  38. Pingback: WiFi Horizon, Best news updates, offers and business opportunities

  39. I m trying to Refresh the list using OnNavigatedTo,, but it do nothing.
    How actually to recreate my list.. I m using another page to add list items to my list, and when i return back,, it dont refresh at all… Help..

    Reply
    • Do you mean regenerate the listbox? You can set the itemssource / datacontext as null and assign back your datasource / datacontext. In general, the list using observablecollection will automatically reflect on the view. 

      Reply
      • Thank u, but still I got a problem
        Here is my code::::::::::::
        protected override void OnNavigatedTo(NavigationEventArgs args)
        {
        commentlist.ItemsSource = null; // As suggested by u.. Still no change.

        var db = new SQLiteConnection(“foo”); // My Database —created in sqlite extension of visual Studio.
        int t = 0;
        // db.CreateTable();

        // I read data From CommentD table

        var sti = db.Query(“select c_Id from CommentD”);
        foreach (var item in sti)
        if (item.c_Id != 0) t = 1; // If CommentD table is not empty, t=1

        if (t == 1)
        {
        string imguri = null;
        int temp = Comm_Id;
        List commlist = new List(); // Creates ListBox

        while (temp != 0)
        {
        var st = db.Query(“select * from CommentD where c_Id = ?”, temp);

        foreach (var item in st)
        {
        if (item.Medal == 1) imguri = “Images/medalass.png”;
        else imguri = “Images/medal1.png”;
        commlist.Add(new Comment(item.Title, item.Medal, item.Owner, imguri));
        }
        var sp = db.Query(“select nxt_Id from CommentD where c_Id = ?”, temp);
        foreach (var item in sp) temp = item.nxt_Id;
        }
        commentlist.ItemsSource = commlist;
        }
        }

        :::::::::::::> I add data in table CommentD table,,, in another page. What I need to do is, when i return back the List must reflect the changes…
        What happens now is,, when i add a Comment and return to this page it show no changes.. But when I close this page and start it again it just show me the required output…

        Reply
        • First, have you check the list content if there is new item When you navigate to the page? Meanwhile please use observablecollection instead of list.

          Sent from Samsung Mobile

          Reply
  40. my main page will contain a list box and i have successfully populated it with JSON details. Choosing an item will go to my details page which also contains a list box an this list box should be populated by parsing another JSON. I managed to parse it and put the contents in a list and also binded it to the list in my details.xaml. But when i execute it the details page does not show anything. Below is my code:

    DetailsPage.xaml

    DetailsPage.xaml.cs

    List TrackList = new List();
    protected override void OnNavigatedTo(NavigationEventArgs e)
    {
    string selectedIndex = “”;
    if (NavigationContext.QueryString.TryGetValue(“selectedItem”, out selectedIndex))
    {
    string jsonData = “”;
    string requestURL = “http://ws.audioscrobbler.com/2.0/?method=geo.gettoptracks&country=united+states&api_key=b3671386fbfe75a495225f7eb263ad91&format=json&location=” + selectedIndex;
    Uri uri = new Uri(requestURL, UriKind.Absolute);
    WebClient client = new WebClient();
    client.Headers[“Content-Type”] = “application/json”;
    client.UploadStringCompleted += new UploadStringCompletedEventHandler(client_RetrieveJsonBuildingDetails);
    client.UploadStringAsync(uri, “POST”, jsonData);
    }
    }
    private void client_RetrieveJsonBuildingDetails(object sender, UploadStringCompletedEventArgs e)
    {
    //Use json.net to get result as a JObject then we can use indexing or LINQ to get the data
    JObject jObject = JObject.Parse(e.Result);

    Tracks t = new Tracks();

    JArray trackarray = (JArray)jObject[“toptracks”][“track”];
    int tcount = trackarray.Count;
    for (int j = 0; j < tcount; j++)
    {
    t.TName = (string)jObject["toptracks"]["track"][j]["name"];
    t.TUrl = (string)jObject["toptracks"]["track"][j]["url"];
    t.AName = (string)jObject["toptracks"]["track"][j]["artist"]["name"];
    t.AUrl = (string)jObject["toptracks"]["track"][j]["artist"]["url"];
    JArray imagearray = (JArray)jObject["toptracks"]["track"][j]["image"];
    int icount = imagearray.Count;
    for (int k = 0; k < icount; k++)
    {
    string size = (string)jObject["toptracks"]["track"][j]["image"][k]["size"];
    t.Image = (string)jObject["toptracks"]["track"][j]["image"][k]["text"];
    if (size.Equals("medium"))
    break;
    }
    this.TrackList.Add(new TrackViewModel()
    {
    TName = t.TName,
    TUrl = t.TUrl,
    Image = t.Image,

    AName = t.AName,
    AUrl = t.AUrl
    });
    }
    }

    TrackViewModel.cs

    class TrackViewModel : INotifyPropertyChanged
    {
    private string tname;
    ///
    /// Sample ViewModel property; this property is used in the view to display its value using a Binding.
    /// –
    ///
    public string TName
    {
    get
    {
    return tname;
    }
    set
    {
    if (value != tname)
    {
    tname = value;
    NotifyPropertyChanged(“TName”);
    }
    }
    }

    private string aname;
    ///
    /// Sample ViewModel property; this property is used in the view to display its value using a Binding.
    ///
    ///
    public string AName
    {
    get
    {
    return aname;
    }
    set
    {
    if (value != aname)
    {
    aname = value;
    NotifyPropertyChanged(“AName”);
    }
    }
    }

    private string turl;
    ///
    /// Sample ViewModel property; this property is used in the view to display its value using a Binding.
    ///
    ///
    public string TUrl
    {
    get
    {
    return turl;
    }
    set
    {
    if (value != turl)
    {
    turl = value;
    NotifyPropertyChanged(“TUrl”);
    }
    }
    }

    private string image;
    ///
    /// Sample ViewModel property; this property is used in the view to display its value using a Binding.
    ///
    ///
    public string Image
    {
    get
    {
    return image;
    }
    set
    {
    if (value != image)
    {
    image = value;
    NotifyPropertyChanged(“Image”);
    }
    }
    }

    private string aurl;
    ///
    /// Sample ViewModel property; this property is used in the view to display its value using a Binding.
    ///
    ///
    public string AUrl
    {
    get
    {
    return aurl;
    }
    set
    {
    if (value != aurl)
    {
    aurl = value;
    NotifyPropertyChanged(“AUrl”);
    }
    }
    }

    public event PropertyChangedEventHandler PropertyChanged;
    private void NotifyPropertyChanged(String propertyName)
    {
    PropertyChangedEventHandler handler = PropertyChanged;
    if (null != handler)
    {
    handler(this, new PropertyChangedEventArgs(propertyName));
    }
    }
    }

    Tracks.cs

    [KnownType(typeof(Tracks))]
    [DataContract] //Gets the outer {}
    public class TracksJson
    {
    [DataMember(Name = “toptracks”)] //Gets the d
    public Tracks toptracks { get; set; }
    }

    [DataContract(Name = “Tracks”, Namespace = “”)]
    public class Tracks
    {
    [DataMember]
    public string TName { get; set; }
    [DataMember]
    public string AName { get; set; }
    [DataMember]
    public string TUrl { get; set; }
    [DataMember]
    public string Image { get; set; }
    [DataMember]
    public string AUrl { get; set; }

    }

    Reply
  41. hello; i added a large text to my listbox in wp7 but it not displaying the complete text, pls help me out

    Reply
  42. Hiya very nice blog!! Guy .. Excellent .. Wonderful .. I’ll bookmark your site and take the feeds also? I am happy to search out so many useful info here within the post, we’d like work out more techniques in this regard, thank you for sharing. . . . . .

    Reply
  43. It’s actually a great and helpful piece of info. I’m satisfied
    that you just shared this useful info with us. Please keep us up to date like this.

    Thanks for sharing.

    Reply
  44. Hi.. Wonderful example. Helps me lots. But, how to write the click events in View Model? I don’t want write the event in Xaml.cs.
    Now i have write like:-

    private void Button_Click_1(object sender, RoutedEventArgs e)
    {
    var myUser = ((Button)sender).DataContext as ListBoxEventsModel;
    MessageBox.Show(“Selected==>” + myUser.FirstName);
    }
    It is working. But i want write this same in ViewModel. Please let me any idea.

    Reply
  45. Pingback: Windows Phone List Foreach | Technology Documents

  46. Pingback: ListBox Binding Issue - Popular Windows Phone Questions

  47. Pingback: WP7 - access to sample data from code

  48. Pingback: wp7, listbox of ~80 items not populating when navigating back to it

  49. Pingback: Windows Phone 7 collapsible list

  50. Pingback: Get list like CNN-app

  51. VMware Certified Advanced Professional 6 (Desktop and Mobility Deployment) – The industry-recognized VCAP6-DTM Deploy certification validates that you know how to deploy and optimize VMware Horizon 6 (with View) environments. It demonstrates that you have the knowledge and expertise essential to leverage best practices to provide a scalable and dependable Business Mobility platform for your organization. Some of the topics involve: Configuring and managing Horizon View components, configuring cloud pod archituecture, configuring Group Policy settings related to Horizon View, Configuring and optimizing desktop images for Horizon View & Mirage, Configuring and managing App Volumes AppStacks, Configuring desktop pools, Configuring and deploying ThinApp packaged applications, Configuring VMWare Identity Manager, etc.Sebastian’s take on the VCAP6 exam: “In my personal viewpoint VCAP6 examination is way better experience in comparison with VCAP5, the newest exam looks exactly like VMware HOL. The interface is simple, questions are well organized on the right area of the screen, and can be hidden to the side or restored when necessary. My bits of advice to the questions windowpane: if you want to make it floating, you must know how to restore it back. I ended up moving it around because I forget how to recover it back. The 2 arrows that looked like buttons on the top were supposed to dock the window to right or left. Fonts can be resized, which in my view was a lot better than scrolling up and down the question. The reaction speed of the entire interface was considerably quicker than VCAP5.5, and there wasn’t any lagging time experienced when transitioning from window to window. Something to remember: BACKSPACE key is not working! I think this is beneficial since you don’t reload your exam window by mistake, yet, it can be frustrating in some cases when you type something wrongly and you need to select and press Del to remove. The Desktop and shortcuts were structured quite nicely, and essential applications like web browser or Mirage console can easily be launched. There’s a decent interface for Remote Desktop Manager and you’ll discover all required RDP connection to servers or desktops without the need to type username and password. The web browser had all the links in the Favorite Bar. Right at that moment I’m penning this, there’s no additional Thirty minute extension for Non-Native English speaker at No-Native English country, which is a bummer. You’ll find thirty-nine question to respond to within the 3 hours time period, which can be actually quite hard for non-native English speakers just like me. Several questions take time to complete, so it’s best to skip out on the questions that you can’t respond to, and finish those you can. At the end of the thirty-nine questions, you’ll be able to resume the uncompleted questions when you have time. DO not waste a long time on one single question! The exam blue print is available on my site at Szumigalski.com. It is well-organized and following it for the examination preparation will be helpful to a lot. Of course, the best is if you could have numerous hands on experience! I’m in fact very pleased with the examination experience, although I passed this time by small margin, however i really know what I missed for the examination, study from the mistakes and practice harder to acquaint myself with the environment. This certificates definitely will open up your career prospects!”

    Reply
  52. Pingback: Volodymyr Mudryk – Статьи для разработчиков WP7 - FIXES Шнурки

  53. Pingback: Windows Phone 7 collapsible list - Tutorial Guruji

  54. Pingback: 30+ Excellent Windows Phone 7 Development Tutorials - 站壳网

Leave a reply to gryphonmaster Cancel reply