I have a virtualized ListBox with a lot of GIFs loading from HDD which play in a loop.
I am using Grid as I plan to add more to the control so please stick with it.
LongFileName is a full path.
public class cThumbnail3 : System.Windows.Controls.Grid
{
public string LongFileName
{
get { return (string)GetValue(LongFileNameProperty); }
set { SetValue(LongFileNameProperty, value); }
}
public static readonly DependencyProperty LongFileNameProperty =
DependencyProperty.Register("LongFileName", typeof(string), typeof(cThumbnail3), new PropertyMetadata(OnLongFileNameChanged));
static void OnLongFileNameChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
cThumbnail3 t = obj as cThumbnail3;
t.LoadAsGif();
}
private MediaElement ME;
public void LoadAsGif()
{
ME = new MediaElement();
ME.UnloadedBehavior = MediaState.Manual;
Uri uri = new Uri(#"file://" + LongFileName);
ME.Source = uri;
ME.MediaEnded += (o, e) =>
{
ME.Position = new TimeSpan(0, 0, 1);
ME.Play();
};
this.Children.Clear();
this.Children.Add(ME);
}
}
xaml is simple for now
<local:cThumbnail3 LongFileName="{Binding FullPath}" />
I am monitoring memory usage as I scroll up and down the listbox. Every time item gets into view it new cThumbnail3 is created and item is playing from the start as it should.
Problem is that after some time Memory consumption gets to 1.2GB and playback stops
EDIT
What else I have tried:
Differently adding event + Unload everything when control is out of the view
private bool IsVisible { get; set; }
public void LoadAsGif()
{
ME = new MediaElement();
ME.UnloadedBehavior = MediaState.Manual;
Uri uri = new Uri(#"file://" + LongFileName);
ME.Source = uri;
ME.MediaEnded += ME_MediaEnded;
}
private void ME_MediaEnded(object sender, RoutedEventArgs e)
{
if (!IsVisible) return;
ME.Position = new TimeSpan(0, 0, 1);
ME.Play();
}
private PropertyChangedEventHandler propertyChanged;
public event PropertyChangedEventHandler PropertyChanged
{
add
{
var wasAttached = propertyChanged != null;
propertyChanged += value;
var isAttached = propertyChanged != null;
if (!wasAttached && isAttached)
OnPropertyChangedAttached();
}
remove
{
var wasAttached = propertyChanged != null;
propertyChanged -= value;
var isAttached = propertyChanged != null;
if (wasAttached && !isAttached)
{
OnPropertyChangedDetached();
}
}
}
void OnPropertyChangedAttached()
{
IsVisible = true;
if (ME != null)
ME.Play();
}
void OnPropertyChangedDetached()
{
IsVisible = false;
if (ME != null)
{
ME.MediaEnded -= ME_MediaEnded;
ME.Stop();
ME.Close();
ME.Source = null;
ME = null;
}
}
You can try to write a complete method for the MediaEnded event and in the destructor of the class you can write -= for the event. I guess the event is the culprit here which may be causing the object to not get disposed properly.
You can also set VirtualizingStackPanel.IsVirtualizing="true" for the ListBox to improve performance. Look for more about it on http://msdn.microsoft.com/en-us/library/system.windows.controls.virtualizingstackpanel.isvirtualizing(v=vs.110).aspx
Related
I thought that my bug was a Xamarin Forms issue because there was no bug in XF3.4, but it appeared after I upgraded to XF4.4.
Just to make sure, I want to show you guys the code. I have a XAML page with the loading icon:
<ActivityIndicator IsRunning="{Binding Loading}"
IsVisible="{Binding Loading}"
AbsoluteLayout.LayoutFlags="All"
AbsoluteLayout.LayoutBounds="0.5, 0.5, 0.2, 0.2">
<ActivityIndicator.Color>
<OnPlatform x:TypeArguments="Color" iOS="#2499CE" WinPhone="#2499CE" />
</ActivityIndicator.Color>
</ActivityIndicator>
The "Loading" boolean is binded in the page model here:
public class MyLoginWebPageModel : BasePageModel
{
private BrowserOptions _options;
private Action<BrowserResult> _trySetResult;
private BrowserResult _result = new BrowserResult() { ResultType = BrowserResultType.UserCancel };
private Boolean _navPopped = false;
public string StartUrl { get; private set; }
public bool Loading { get; set; } = false; // RIGHT HERE!!!!!!!!!!!!!!!!!!!!!!
public OidcLoginWebPageModel(ICoreDataRepository repository, ILoginProvider loginProvider, ICache cache, IEventTrace trace, IUsageTimer usageTimer, IPlatform platform)
: base(loginProvider, cache, trace, usageTimer, platform){}
public override void Init(object initData)
{
base.Init(initData);
Tuple<BrowserOptions, Action<BrowserResult>> initObject = initData as Tuple<BrowserOptions, Action<BrowserResult>>;
_options = initObject.Item1;
_trySetResult = initObject.Item2;
StartUrl = _options.StartUrl;
}
protected override void OnPageWasPopped(object sender, EventArgs e)
{
base.OnPageWasPopped(sender, e);
_trySetResult(_result);
}
internal async Task OnBrowserNavigated(object sender, WebNavigatedEventArgs e)
{
Loading = false;
if (!(sender is WebView browser))
{
throw new Exception($"Sender is not of type WebView");
}
if (!Uri.TryCreate(e.Url, UriKind.Absolute, out Uri uri))
{
throw new Exception($"Uri creation failed for: {e.Url}");
}
if (string.IsNullOrEmpty(_options.EndUrl))
{
if (uri.LocalPath.ToLowerInvariant() == "/account/logout")
{
_result = new BrowserResult() { ResultType = BrowserResultType.Success };
if (!_navPopped)
{
_navPopped = true;
await PopPageModel();
}
}
}
}
internal async Task OnBrowserNavigating(object sender, WebNavigatingEventArgs e)
{
Loading = true;
if (!(sender is WebView browser))
{
throw new Exception($"Sender is not of type WebView");
}
if (!Uri.TryCreate(e.Url, UriKind.Absolute, out Uri uri))
{
throw new Exception($"Uri creation failed for: {e.Url}");
}
if (string.IsNullOrEmpty(_options.EndUrl) == false)
{
if (uri.AbsoluteUri.StartsWith(_options.EndUrl))
{
_result = new BrowserResult() { ResultType = BrowserResultType.Success, Response = uri.Fragment.Substring(1) };
e.Cancel = true;
if (!_navPopped)
{
_navPopped = true;
Loading = false;
await PopPageModel();
}
}
}
}
}
Is there anything in here that would indicate a loading icon not disappearing at all?
Thanks!
edit: So this is what I'm thinking I need to do.
First I change my boolean situation
private bool Loading = false;
public bool currentlyLoading
{
get { return Loading; }
set
{
currentlyLoading = Loading;
onPropertyChanged();
}
}
Then in the same file I implement the onPropertyChanged() function to.. let the Bindable property in my xaml file know that the property has changed?
Is this a good implementation?
// Option 1
protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
// Option 2
protected void OnPropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
your class (or it's base class) needs to implement INotifyPropertyChanged. Then your Loading property would look something like this
private bool loading = false;
public bool Loading
{
get { return loading; }
set
{
loading = value;
OnPropertyChanged();
}
}
Option 1 is best. But it would be good if you move your property changed logic to BasePageModel.
Try this one:
private bool _loading { get; set; }
public bool Loading { get {return _loading; } set{value = _loading } }
And in your OnBrowserNavigating:
internal async Task OnBrowserNavigating(object sender, WebNavigatingEventArgs e)
{
Loading = true;
if (!(sender is WebView browser))
{
Loading = false;
throw new Exception($"Sender is not of type WebView");
}
if (!Uri.TryCreate(e.Url, UriKind.Absolute, out Uri uri))
{
Loading = false;
throw new Exception($"Uri creation failed for: {e.Url}");
}
if (string.IsNullOrEmpty(_options.EndUrl) == false) //IF THE CONDITION OVER HERE & FOR INNER IF CONDITIONS FAILS, LOADER WASNT SET TO FALSE
{
if (uri.AbsoluteUri.StartsWith(_options.EndUrl))
{
_result = new BrowserResult() { ResultType = BrowserResultType.Success, Response = uri.Fragment.Substring(1) };
e.Cancel = true;
if (!_navPopped)
{
_navPopped = true;
Loading = false;
await PopPageModel();
}
}
}
Loading = false;
}
I am trying to make a display of my items listed on the right side of my window. When I first initialize my window, everything works perfectly. What I do is, I deserialize a file and get my ObservableCollection<Model.Resources>. All the items from my list are shown in the ListBox.
When I enter my Add Resource window, and add some more resources, I serialize those objects into a file. When I am finished with adding, I call a method called refresh() that deserializes some files and updates my ObservableCollection<Model.Resources>. When I am finished with that, ListBox doesn't update until I restart my program.
XAML of my Window that has ListBox:
<ListBox x:Name="itemList" Grid.Column="1" HorizontalAlignment="Stretch" Margin="7,23,6,0" Grid.Row="1" VerticalAlignment="Stretch" Background="#324251" ItemsSource="{Binding Path=resources}" FontSize="16" Foreground="Wheat"/>
Relevant code of my Window class:
public partial class GlowingEarth : Window, INotifyPropertyChanged
{
private ObservableCollection<Model.Etiquette> _tags;
private ObservableCollection<Model.Resource> _resources;
private ObservableCollection<Model.Type> _types;
public ObservableCollection<Model.Etiquette> tags
{
get
{
return _tags;
}
set
{
_tags = value;
OnPropertyChanged("tags");
}
}
public ObservableCollection<Model.Resource> resources
{
get
{
return _resources;
}
set
{
_resources = value;
OnPropertyChanged("resources");
}
}
public ObservableCollection<Model.Type> types
{
get
{
return _types;
}
set
{
_types = value;
OnPropertyChanged("types");
}
}
private BinaryFormatter fm;
private FileStream sm = null;
private string path;
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
public GlowingEarth()
{
InitializeComponent();
tags = new ObservableCollection<Model.Etiquette>();
resources = new ObservableCollection<Model.Resource>();
types = new ObservableCollection<Model.Type>();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
refresh();
}
public void refresh()
{
path = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "typeList");
if (File.Exists(path))
{
fm = new BinaryFormatter();
sm = File.OpenRead(path);
_types = (ObservableCollection<Model.Type>)fm.Deserialize(sm);
sm.Dispose();
}
else
{
return;
}
path = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "tagList");
if (File.Exists(path))
{
fm = new BinaryFormatter();
sm = File.OpenRead(path);
_tags = (ObservableCollection<Model.Etiquette>)fm.Deserialize(sm);
sm.Dispose();
}
else
{
return;
}
path = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "reslist");
if (File.Exists(path))
{
fm = new BinaryFormatter();
sm = File.OpenRead(path);
_resources = (ObservableCollection<Model.Resource>)fm.Deserialize(sm);
sm.Dispose();
}
else
{
return;
}
InitializeComponent();
this.DataContext = this;
}
Can you please tell me, what on earth is going on, and why isn't my list updating?
in refresh() method you change the value of a field
_resources = (ObservableCollection<Model.Resource>)fm.Deserialize(sm);
it does not call OnPropertyChanged("resources");
you should use resources property
resources = (ObservableCollection<Model.Resource>)fm.Deserialize(sm);
same for _types and _tags
Hi,
I'm struggling a bit using the ListBox.DataSource and the INotifyPropertyChanged Interface. I checked several posts about this issue already but I cannot figure out, how to update the view of a ListBox if an element of the bound BindingList is changed.
I basically want to change the color of an IndexItem after the content has been parsed.
Here the relevant calls in my form:
btn_indexAddItem.Click += new EventHandler(btn_indexAddItem_Click);
lst_index.DataSource = Indexer.Items;
lst_index.DisplayMember = "Url";
lst_index.DrawItem += new DrawItemEventHandler(lst_index_DrawItem);
private void btn_indexAddItem_Click(object sender, EventArgs e)
{
Indexer.AddSingleURL(txt_indexAddItem.Text);
}
private void lst_index_DrawItem(object sender, DrawItemEventArgs e)
{
IndexItem item = lst_index.Items[e.Index] as IndexItem;
if (item != null)
{
e.DrawBackground();
SolidBrush brush = new SolidBrush((item.hasContent) ? SystemColors.WindowText : SystemColors.ControlDark);
e.Graphics.DrawString(item.Url, lst_index.Font, brush, 0, e.Index * lst_index.ItemHeight);
e.DrawFocusRectangle();
}
}
Indexer.cs:
class Indexer
{
public BindingList<IndexItem> Items { get; }
private object SyncItems = new object();
public Indexer()
{
Items = new BindingList<IndexItem>();
}
public void AddSingleURL(string url)
{
IndexItem item = new IndexItem(url);
if (!Items.Contains(item))
{
lock (SyncItems)
{
Items.Add(item);
}
new Thread(new ThreadStart(() =>
{
// time consuming parsing
Thread.Sleep(5000);
string content = item.Url;
lock (SyncItems)
{
Items[Items.IndexOf(item)].Content = content;
}
}
)).Start();
}
}
}
IndexItem.cs
class IndexItem : IEquatable<IndexItem>, INotifyPropertyChanged
{
public int Key { get; }
public string Url { get; }
public bool hasContent { get { return (_content != null); } }
private string _content;
public string Content {
get
{
return (hasContent) ? _content : "empty";
}
set
{
_content = value;
ContentChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void ContentChanged()
{
if (this.PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("Content"));
}
}
public IndexItem(string url)
{
this.Key = url.GetHashCode();
this.Url = url;
}
public override bool Equals(object obj)
{
return Equals(obj as IndexItem);
}
public override int GetHashCode()
{
return Key;
}
public bool Equals(IndexItem other)
{
if (other == null) return false;
return (this.Key.Equals(other.Key)) ||
((hasContent || other.hasContent) && (this._content.Equals(other._content)));
}
public override string ToString()
{
return Url;
}
}
Any ideas what went wrong and how to fix it? I'll appreciate any hint...
It seems to me that the control should redraw when it raises the ListChanged event for that item. This will force it to do so:
lst_index.DrawItem += new DrawItemEventHandler(lst_index_DrawItem);
Indexer.Items.ListChanged += Items_ListChanged;
private void Items_ListChanged(object sender, ListChangedEventArgs e)
{
lst_index.Invalidate(); // Force the control to redraw when any elements change
}
So why doesn't it do that already? Well, it seems that the listbox only calls DrawItem if both DisplayMember changed, and if the INotifyPropertyChanged event was raised from the UI thread. So this also works:
lock (SyncItems)
{
// Hacky way to do an Invoke
Application.OpenForms[0].Invoke((Action)(() =>
{
Items[Items.IndexOf(item)].Url += " "; // Force listbox to call DrawItem by changing the DisplayMember
Items[Items.IndexOf(item)].Content = content;
}));
}
Note that calling PropertyChanged on the Url is not sufficient. The value must actually change. This tells me that the listbox is caching those values. :-(
(Tested with VS2015 REL)
I suddenly found a strange behavior of collection implementing ISupportIncrementalLoading.
Let's say we have a main page with ISupportIncrementalLoading collection bound to ListView. And we have another page where we can navigate to.
When navigating to main page, the ISupportIncrementalLoading starts loading items until ListView thinks it's enough. I navigate to new page BEFORE ListView loaded all items it needs.
My expected behavior: ListView stops loading new items as the page isn't visible now.
Real behavior: ListView continues to load items endlessly, even after going away from the page. And it won't stop until gets HasMore == false.
Can anyone help with this? This is absolutely wrong behavior.
PS
If I while navigation, set in ViewModel the collection to null and then restore it when coming back -- it seams to help, but that is too much to do, I think.
Here's the code of my basic ISupportIncrementalLoading collection:
public abstract class BaseIncrementalSupportCollection<T> :IList<T>,IList,INotifyCollectionChanged, ISupportIncrementalLoading, INotifyPropertyChanged
{
protected readonly List<T> storage;
private bool isLoading;
public bool IsLoading
{
get
{
return isLoading;
}
set
{
if (isLoading != value)
{
isLoading = value;
RaisePropertyChanged();
}
}
}
public bool failed;
public bool IsFailed
{
get { return failed; }
set
{
if (failed != value)
{
failed = value;
RaisePropertyChanged();
}
}
}
public bool IsEmpty
{
get { return !HasMoreItems && Count == 0; }
}
protected BaseIncrementalSupportCollection()
{
storage = new List<T>();
}
public virtual IAsyncOperation<LoadMoreItemsResult> LoadMoreItemsAsync(uint count)
{
return Task.Run(()=>LoadMoreItems(count)).AsAsyncOperation();
}
public abstract bool HasMoreItems { get; }
private async Task<LoadMoreItemsResult> LoadMoreItems(uint count)
{
IsLoading = true;
IsFailed = false;
try
{
var items = await LoadMoreItemsOverride(count);
if (items == null)
return new LoadMoreItemsResult() {Count = 0};
if (items.Count > 0)
{
var prevEmptyState = IsEmpty;
foreach (var item in items)
{
var currItem = item;
await DispatchHelper.RunOnUiIfNecessary(async () =>
{
storage.Add(currItem);
RaiseCollectionChanged(
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, currItem,
storage.Count - 1));
});
}
if(prevEmptyState!=IsEmpty)
RaisePropertyChanged("IsEmpty");
}
return new LoadMoreItemsResult() {Count = (uint) items.Count};
}
catch (Exception e)
{
var aggregate = e as AggregateException;
if (aggregate != null)
e = aggregate.Flatten().InnerException;
IsFailed = true;
var handler = OnError;
if (handler != null)
DispatchHelper.RunOnUiIfNecessary(
() => handler(this, new IncrementallCollectionLoadErrorEventArgs(e)));
return new LoadMoreItemsResult() {Count = 0};
}
finally
{
IsLoading = false;
}
}
protected virtual void RaiseCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (CollectionChanged != null)
CollectionChanged(this, e);
}
protected abstract Task<IList<T>> LoadMoreItemsOverride(uint count);
[NotifyPropertyChangedInvocator]
protected virtual void RaisePropertyChanged([CallerMemberName] string propertyName = null)
{
var handler = PropertyChanged;
if (handler != null)
DispatchHelper.RunOnUiIfNecessary(()=>handler(this, new PropertyChangedEventArgs(propertyName)));
}
public event PropertyChangedEventHandler PropertyChanged;
public event EventHandler<IncrementallCollectionLoadErrorEventArgs> OnError;
public event NotifyCollectionChangedEventHandler CollectionChanged;
}
I've just found another way, adding additional bool field isStopped with methods Start, ForceStop setting it to false/true. This value is used when getting HasMoreItems like
bool HasMoreItems{get{return !isStopped && DetermineIfHasMore()};}
And simply by calling those to methods I can stop or continue loading the same collection generator.
Another way is provided here https://social.msdn.microsoft.com/Forums/ru-RU/be17357d-faac-4f49-acf4-e916fcdace9d/w81isupportincrementalloading-doesnt-stop-after-navigating-away?forum=winappswithcsharp
We're stuck on an issue about the appropriate use of SizeToContent=WidthandHeight and WindowStartupLocation=CenterScreen in WPF. After resizing, our window has strange black border and it is not at the center.
The code does not work until MaxWith and MaxHeight is given.
To fix this, I used the RestoreBounds instead:
var previosWidth = this.RestoreBounds.Width;
var previosHeight = this.RestoreBounds.Height;
We have solved it with this class. You should use it instead of common Window.
public class CustomizableChromeWindow : Window, INotifyPropertyChanged
{
protected override void OnStateChanged(EventArgs e)
{
base.OnStateChanged(e);
OnPropertyChanged("CaptionButtonMargin");
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
HandleSizeToContent();
}
private void HandleSizeToContent()
{
if (this.SizeToContent == SizeToContent.Manual)
return;
var previosTopXPosition = this.Left;
var previosTopYPosition = this.Top;
var previosWidth = this.MaxWidth;
var previosHeight = this.MaxHeight;
var previousWindowStartupLocation = this.WindowStartupLocation;
var previousSizeToContent = SizeToContent;
SizeToContent = SizeToContent.Manual;
Dispatcher.BeginInvoke(
DispatcherPriority.Loaded,
(Action)(() =>
{
this.SizeToContent = previousSizeToContent;
this.WindowStartupLocation = WindowStartupLocation.Manual;
this.Left = previosTopXPosition + (previosWidth - this.ActualWidth)/2;
this.Top = previosTopYPosition + (previosHeight - this.ActualHeight) / 2;
this.WindowStartupLocation = previousWindowStartupLocation;
}));
}
public Thickness CaptionButtonMargin
{
get
{
return new Thickness(0, 0, 0, 0);
}
}
#region INotifyPropertyChanged
private void OnPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}