1

I'm working on a messenger program and I have a timer which constantly deletes and adds new list box items so the list box flickers all the time. I'm trying to make the flickering stop. The reason I'm constantly deleting and adding new list box items is because if a friend logs in, it will change there status from offline to online.

Timer code:

private void Requests_Tick(object sender, EventArgs e)
{
      LoadData();
}

LoadData() code:

FriendsLb.BeginUpdate();
_S = new Status();
Image Status = null;
FriendsLb.Items.Clear();
try
{
    var query = from o in Globals.DB.Friends
                where o.UserEmail == Properties.Settings.Default.Email
                select new
                {
                    FirstName = o.FirstName,
                    LastName = o.LastName,
                    Email = o.Email,
                    Status = o.Status,
                    Display = string.Format("{0} {1} - ({2})", o.FirstName, o.LastName, o.Email)
                };
    newFriendsLb.DataSource = query.ToList();
    newFriendsLb.ClearSelected();
    FriendsLb.DrawMode = DrawMode.OwnerDrawVariable;

    foreach (object contact in query.ToList())
    {
        string details = contact.GetType().GetProperty("Display").GetValue(contact, null).ToString();
        string email = contact.GetType().GetProperty("Email").GetValue(contact, null).ToString();
        string status = _S.LoadStatus(email);

        if (status == "Online")
        {
            Status = Properties.Resources.online;
        }
        else if (status == "Away")
        {
            Status = Properties.Resources.busy;
        }
        else if (status == "Busy")
        {
            Status = Properties.Resources.away;
        }
        else if (status == "Offline")
        {
            Status = Properties.Resources.offline;
        }
        FriendsLb.Items.Add(new Listbox(_A.LoadFriendAvatar(email), Status, details));
    }
    contact = query.ToList();
    FriendsLb.MeasureItem += FriendsLb_MeasureItem;
    FriendsLb.DrawItem += FriendsLb_DrawItem;
    FriendsLb.EndUpdate();

Is there a way to update the current list box items constantly rather than constantly deleting and adding new ones?

Here's the GUI:

GUI

Ňɏssa Pøngjǣrdenlarp
  • 38,411
  • 12
  • 59
  • 178
richardj97
  • 79
  • 1
  • 10
  • Why not use an eventhandler for the `Status` Property. That way the update will only be invoked if there's a change? – Barry O'Kane Jul 06 '16 at 12:46
  • You are not just adding and deleting - you are clearing the list and completely repopulating it at some interval. – Ňɏssa Pøngjǣrdenlarp Jul 06 '16 at 12:47
  • Instead of complete redraw on query you can detect changes and only update affected item. For this you need some key field (e.g. email? is it unique?). Don't `Clear()`, but go through items and update them. – Sinatr Jul 06 '16 at 12:47
  • That's the problem, I don't really know how to update them other than deleting and repopulating them :/ – richardj97 Jul 06 '16 at 12:51
  • a) Use a BindingList perhaps with a concrete class. changes to the list contents will show in the control. B) Perhaps implement INotifyPropertyChanged C) then just update the list D) I think you want to load those status indicator images to an array - your app is likely leaking – Ňɏssa Pøngjǣrdenlarp Jul 06 '16 at 12:54
  • What kind of control is this? A ListBox? There is an [Items property](https://msdn.microsoft.com/en-us/library/system.windows.forms.listbox.items.aspx) that returns a `ListBox.ObjectCollection` representing the items in the ListBox. You can modify the individual items that way. – Cody Gray - on strike Jul 06 '16 at 12:55
  • It seems you don't read [comments](http://stackoverflow.com/questions/38173237/c-sharp-object-reference-not-set-to-an-instance-of-an-object-get-type-get-pr#comment63774078_38173237) carefully. For example you are still using reflection to read values from query while you don't need. Also it seems you don't know about data-binding and setting `DataSource` of `ListBox`. – Reza Aghaei Jul 06 '16 at 13:19

2 Answers2

3

The are several ways to remove the flicker - all basically involve not completely repopulating the list each time. For this, you want to get the current status for the users and simply update the existing list.

In order for the control to see changes to the list items, rather than an anonymous type, you need a User class so that you can implement INotifyPropertyChanged. This "broadcasts" a notice that a property value has changed. You will also need to use a BindingList<T> so those messages get forwarded to the control. This will also allow additions/deletions from the list to be reflected.

You will also need a concrete way to find each user, so the class will need some sort of ID.

public enum UserStatus { Unknown, Online, Offline, Away, Busy }

class User : INotifyPropertyChanged 
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public Image StatusImage;

    private UserStatus status = UserStatus.Unknown;
    public UserStatus Status 
    { 
        get{return status;}
        set{
            if (value != status)
            {
                status=value;
                PropertyChanged(this, new PropertyChangedEventArgs("Status"));
            }
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    public override string ToString()
    {
         return string.Format("{0}, {1}: {2}", LastName, FirstName, Status);
    }

}

Then the collection:

private BindingList<User> Users;
private Image[] StatusImgs;          // See notes

The BindingList is then used as the DataSource for the control:

Users = GetUserList();

// display the list contents in the listbox:
lbUsers.DataSource = Users;
timer1.Enabled = true;

Updating the user status just involves resetting the Status on each user which has changed. The BindingList<User> will then notify the control to update the display:

private void UpdateUserStatus()
{
    // get current list of user and status
    var newStatus = GetCurrentStatus();
    User thisUser;

    // find the changed user and update
    foreach (User u in newStatus)
    {          
        thisUser = Users.FirstOrDefault(q => q.Id == u.Id);
        // ToDo: If null, there is a new user in the list: add them.
        if (thisUser != null && thisUser.Status != u.Status)
        { 
            thisUser.Status = u.Status;
            thisUser.StatusImage = StatusImgs[(int)u.Status];
        }
    }
}

Results:

enter image description here


Note that there is a potential leak in your app. If you drill into the code to get an image from Resources you will see:

internal static System.Drawing.Bitmap ball_green {
    get {
        object obj = ResourceManager.GetObject("ball_green", resourceCulture);
        return ((System.Drawing.Bitmap)(obj));
    }
}

GetObject() is creating a new object/image each time you call it, your code doesnt show the old one being Disposed() so, it is likely leaking resources.

Since each online user doesn't need their own unique instance (or a new one when the status changes), load them once into a List or array so they can be reused:

// storage:
private Image[] StatusImgs;
...
// populate:
StatusImgs = new Image[] {Resources.ball_black, Resources.ball_green, 
            Resources.ball_red, Resources.ball_yellow, Resources.ball_delete};
...
// usage:
thisUser.StatusImage = StatusImgs[(int)u.Status];

You could also change it so the User class updates that itself when the Status changes.

Finally, you might want to consider a simple UserControl for the UI rather than what appears to be an owner drawn Listbox.

Community
  • 1
  • 1
Ňɏssa Pøngjǣrdenlarp
  • 38,411
  • 12
  • 59
  • 178
  • 1
    @richardj97 The answer is exactly what you need and it has a lot of new and useful things for you which will help you to be in the right direction. When you accept an answer it would be great to also vote for answer. It's reasonable and recommended. Take a look at this post: [Accepting Answers: How does it work?](http://meta.stackexchange.com/a/5235) – Reza Aghaei Jul 06 '16 at 17:04
  • All seems to be working but I don't know what to put in the timer because when I tried it doesn't update the list box, it only updates if I re-open the application. – richardj97 Jul 08 '16 at 14:04
  • Literally all I did in the demo was call `UpdateUserStatus();` from a timer event. As I said. I think you need it implement to `ToDo` - at the start there may be no users in the list, so it needs to add them. That also allows additions to the list to show up – Ňɏssa Pøngjǣrdenlarp Jul 08 '16 at 14:11
0

If you don't want to change your code structure to eliminate the repeated Clear/Reload cycle, you should suspend UI drawing while you are rebuilding your list using;

using(var d = Dispatcher.DisableProcessing())
{
    /* your work...  */
}

As suggested here In WPF, what is the equivalent of Suspend/ResumeLayout() and BackgroundWorker() from Windows Forms

Community
  • 1
  • 1
PhillipH
  • 6,182
  • 1
  • 15
  • 25