2010-07-28 6 views
8

Non ci sono molte opzioni per un pannello di avvolgimento virtualizzante da utilizzare in WPF. Per un motivo o per un altro SM decise di non spedirne uno nella biblioteca standard.Virtualizzazione del pannello Wrap WPF Edizione

Se qualcuno potesse essere così audace da fornire una risposta fonte folla (e spiegazione) al primo elemento di lavoro il seguente progetto CodePlex, sarei molto grato:

http://virtualwrappanel.codeplex.com/workitem/1

Grazie!


Sintesi di emissione:

Recentemente ho provato ad utilizzare il WrapPanel Virtualizzazione da questo progetto e ho incontrato un bug.

Procedura per riprodurre:

  1. Creare casella di riepilogo.
  2. Imposta il wrappanel di virtualizzazione come itemhost in un modello listboxpanel.
  3. Associare l'origine elementi della listbox a una raccolta osservabile.
  4. Rimuovere un elemento dalla raccolta di osservazioni di supporto.

Il Debug.Assert fallisce (Debug.Assert (bambino == _children [childIndex], "figlio errato è stato generato");) in MeasureOverride, e continuato risultati di esecuzione in un'eccezione nullo nel metodo di pulizia [vedi screenshot allegato].

Per favore fatemi sapere se siete in grado di correggere questo.

Grazie,

AO


Codice:

http://virtualwrappanel.codeplex.com/SourceControl/list/changesets#

alt text http://virtualwrappanel.codeplex.com/Project/Download/AttachmentDownload.ashx?ProjectName=virtualwrappanel&WorkItemId=1&FileAttachmentId=138959

risposta

4

Il metodo OnItemsChanged deve gestire correttamente i parametri args. Si prega di consultare questo question per ulteriori informazioni. Copiare il codice da questa domanda, si avrebbe bisogno di aggiornare OnItemsChanged in questo modo:

protected override void OnItemsChanged(object sender, ItemsChangedEventArgs args) { 
    base.OnItemsChanged(sender, args); 
    _abstractPanel = null; 
    ResetScrollInfo(); 

    // ...ADD THIS... 
    switch (args.Action) { 
     case NotifyCollectionChangedAction.Remove: 
     case NotifyCollectionChangedAction.Replace: 
      RemoveInternalChildRange(args.Position.Index, args.ItemUICount); 
      break; 
     case NotifyCollectionChangedAction.Move: 
      RemoveInternalChildRange(args.OldPosition.Index, args.ItemUICount); 
      break; 
    } 
} 
0

In primo luogo, fate attenzione che, in generale, se si rimuove un oggetto da collezione e tu non hai il suo riferimento, quello l'oggetto è morto al momento della rimozione. Quindi, almeno la chiamata RemoveInternalChildRange è illegale dopo la rimozione, ma non è il problema principale.

In secondo luogo, si potrebbe avere una piccola condizione di competizione, anche se non è strettamente multi-thread. È necessario verificare (con breakpoint) se il gestore di eventi reagisce troppo impazientemente: non si desidera che il gestore eventi venga eseguito mentre si è ancora nel mezzo di una rimozione, anche se si tratta di un singolo elemento.

In terzo luogo, verificare la presenza di nulla dopo:

UIElement child = _generator.GenerateNext(out newlyRealized) as UIElement; 

e per la prima prova modificare il codice per avere un'uscita elegante, che in questo caso significa gracefull continua - sono da utilizzare per loop e incrementi nel ciclo essere in grado di continuare a tutto

Controllare anche InternalChildren in cui viene visualizzato null per vedere se quel percorso di accesso restituisce lo stesso risultato di _children (come dimensione, dati interni, null nello stesso posto).

Se si salta un null sopravvive (esegue il rendering senza eccezioni) interromperlo nel debugger subito dopo e verificare se questi array/raccolte sono stati sistemati (nessun valore null all'interno).

Inoltre, pubblicare il progetto di esempio completamente compilabile che fornisce la repro (come file zip) da qualche parte - riduce le assumprions casuali e consente a ppl di creare/eseguire e vedere solo.

A proposito di ipotesi: controlla cosa sta facendo la tua "collezione osservabile". Se si rimuove un elemento da una raccolta, ogni iteratore/enumeratore da uno stato precedente di quella raccolta ha il diritto di lanciare o fornire valori nulli e in un'interfaccia utente che tenta di essere troppo intelligente, può verificarsi un iteratore stantio. facilmente.

8

spiegazione del problema

Avete chiesto una spiegazione di ciò che sta andando male, così come le istruzioni su come risolvere il problema esso. Finora nessuno ha spiegato il problema. Lo farò.

In ListBox con un VirtualizingWrapPanel ci sono cinque strutture separate di dati che tracciano gli elementi, ognuno in modo diverso:

  1. ItemsSource: La collezione originale (in questo caso ObservableCollection)
  2. CollectionView: Mantiene una lista separata di elementi ordinati/filtrati/raggruppati (solo se una di queste funzioni è in uso)
  3. ItemContainerGenerator: traccia la mappatura tra elementi e contenitori
  4. InternalChildren: traccia i contenitori che sono attualmente visibili Le
  5. WrapPanelAbstraction: canzoni che appaiono contenitori su cui si allineano

Quando un elemento viene rimosso da ItemsSource, tale rimozione deve essere propagata attraverso tutte le strutture di dati.Ecco come funziona:

  1. Si chiama Remove() sul ItemsSource
  2. ItemsSource rimuove l'elemento e spara la sua CollectionChanged che viene gestita dal CollectionView
  3. CollectionView rimuove la voce (se l'ordinamento/filtraggio/raggruppamento è in uso) e spara suo CollectionChanged che viene gestita dal ItemContainerGenerator
  4. ItemContainerGenerator aggiorna la sua mappatura, incendi sua ItemsChanged che è gestito da VirtualizingPanel
  5. VirtualizingPanel chiama il metodo OnItemsChanged virtuale whic h è attuato da VirtualizingWrapPanel
  6. VirtualizingWrapPanel scarta il WrapPanelAbstraction in modo che sarà costruito, ma non è mai aggiorna InternalChildren

A causa di questo, la collezione InternalChildren non è sincronizzato con le altre quattro collezioni, portando a gli errori che sono stati vissuti.

soluzione al problema

Per risolvere il problema, aggiungere il seguente codice in qualsiasi punto all'interno metodo OnItemsChanged di VirtualizingWrapPanel:

switch(args.Action) 
{ 
    case NotifyCollectionChangedAction.Remove: 
    case NotifyCollectionChangedAction.Replace: 
     RemoveInternalChildRange(args.Position.Index, args.ItemUICount); 
     break; 
    case NotifyCollectionChangedAction.Move: 
     RemoveInternalChildRange(args.OldPosition.Index, args.ItemUICount); 
     break; 
} 

questo mantiene la collezione InternalChildren in sincronia con le altre strutture di dati.

Perché AddInternalChild/InsertInternalChild non è chiamato qui

si potrebbe chiedere perché non ci sono chiamate a InsertInternalChild o AddInternalChild nel codice sopra, e soprattutto perché la manipolazione Sostituire e Move non ci richiedono di aggiungere un nuovo elemento durante OnItemsChanged.

La chiave per capire questo è nel modo in cui funziona ItemContainerGenerator.

Quando ItemContainerGenerator riceve un evento elimina gestisce tutto e subito:

  1. ItemContainerGenerator rimuove immediatamente la voce dalle proprie strutture di dati
  2. ItemContainerGenerator genera l'evento ItemChanged. Il pannello dovrebbe rimuovere immediatamente il contenitore.
  3. ItemContainerGenerator "unprepares" contenitore rimuovendo il DataContext

D'altra parte, ItemContainerGenerator apprende che un elemento viene aggiunto tutto è tipicamente differito:

  1. ItemContainerGenerator aggiunge immediatamente una "slot" per l'elemento nella sua struttura dati ma non crea un contenitore
  2. ItemContainerGenerator attiva l'evento ItemChanged. Il pannello chiama InvalidateMeasure() [questo viene fatto dalla classe base - non è necessario farlo]
  3. Successivamente, quando viene chiamato MeasureOverride, Generator.StartAt/MoveNext viene utilizzato per generare i contenitori dell'elemento.Ogni contenitore appena generato viene aggiunto a InternalChildren in quel momento.

Così, tutte le rimozioni della collezione InternalChildren (compresi quelli che fanno parte di un movimento o sostituire) deve essere fatto all'interno OnItemsChanged, ma aggiunte possono (e dovrebbero) essere differiti fino al prossimo MeasureOverride.

+0

Sembra che Tom Goff abbia dato il codice necessario mentre scrivevo la mia risposta. Anche la sua risposta è corretta, ed è essenzialmente uguale alla mia senza la spiegazione dettagliata. –

+0

Ciao Ray - Bel riassunto, hai avuto il mio voto. Un problema con la tua risposta è che il problema non è proprio il fatto che "la collezione InternalChildren non sia sincronizzata con le altre quattro raccolte", ma sono sicuro che non aiuta. Il problema di fondo è che i bambini realizzati non vengono "ripuliti". Se rimuovi l'oggetto dall'indice 10, l'elemento all'indice 11 verrà spostato nell'indice 10. Quando vai a realizzare l'oggetto all'indice 10 (che in precedenza era 11), ti ritroverai con l'affermazione "Bambino sbagliato è stato generato ", poiché l'altro bambino non è mai stato realizzato. – CodeNaked