Example #1
0
        void ArrangeChildRows(ArrangeInfo p_info, TvItem p_item)
        {
            if (p_item.Children.Count == 0)
            {
                return;
            }

            if (p_item.IsExpanded)
            {
                foreach (var item in p_item.Children)
                {
                    var elem = (UIElement)Children[p_info.Index];
                    elem.Arrange(new Rect(0, p_info.Top, p_info.FinalSize.Width, elem.DesiredSize.Height));
                    p_info.Top += elem.DesiredSize.Height;
                    p_info.Index++;
                    ArrangeChildRows(p_info, item);
                }
            }
            else
            {
                foreach (var item in p_item.Children)
                {
                    var elem = (UIElement)Children[p_info.Index];
                    elem.Arrange(_rcEmpty);
                    p_info.Index++;
                    ArrangeChildRows(p_info, item);
                }
            }
        }
Example #2
0
        public MainContentResponse(TvItem item)
        {
            id             = item.ID;
            groupID        = (int)Location.Kosice;
            timeInserted   = item.TimeInserted.ToString("yyyy-MM-dd hh:mm:ss.FFF", CultureInfo.InvariantCulture);
            expired        = DateTime.Compare(DateTime.Now, item.EndTime) > 0;
            published      = true;
            editEnabled    = false;
            author         = item.Author;
            headline       = item.Title;
            duration       = item.Duration;
            startDate      = item.StartTime.ToString("yyyy-MM-dd hh:mm:ss.FFF", CultureInfo.InvariantCulture);
            endDate        = item.EndTime.ToString("yyyy-MM-dd hh:mm:ss.FFF", CultureInfo.InvariantCulture);
            important      = true;
            formattedStart = item.StartTime.Ticks;
            formattedEnd   = item.EndTime.Ticks;

            locations = new List <McLocation>();
            foreach (TvItemLocation itemLocation in item.Locations)
            {
                locations.Add(new McLocation(itemLocation));
            }

            fileInfoList = new List <McFileInfo>();
            foreach (TvItemFile itemFile in item.Files)
            {
                fileInfoList.Add(new McFileInfo(itemFile, item.Duration));
            }
        }
Example #3
0
        void MeasureChildRows(MeasureInfo p_info, TvItem p_item)
        {
            if (p_item.Children.Count == 0)
            {
                return;
            }

            if (p_item.IsExpanded)
            {
                foreach (var item in p_item.Children)
                {
                    var elem = (UIElement)Children[p_info.Index];
                    elem.Measure(p_info.Size);
                    p_info.TotalHeight += elem.DesiredSize.Height;
                    p_info.Index++;
                    MeasureChildRows(p_info, item);
                }
            }
            else
            {
                foreach (var item in p_item.Children)
                {
                    var elem = (UIElement)Children[p_info.Index];
                    elem.Measure(_sizeEmpty);
                    p_info.Index++;
                    MeasureChildRows(p_info, item);
                }
            }
        }
Example #4
0
        public Task <bool> SaveVideoFileAsync(TvItem tvItem, IFormFile file)
        {
            string     filename = tvItem.ID + "_" + Guid.NewGuid() + Path.GetExtension(file.FileName);
            TvItemFile itemFile = new TvItemFile()
            {
                FileName = filename,
                Length   = file.Length,
                TvItemId = tvItem.ID
            };

            using (var fileStream = new FileStream(itemFile.AbsolutePath, FileMode.Create))
            {
                file.CopyTo(fileStream);
            }

            if (tvItem.Duration == 0)
            {
                throw new Exception("Video duration is 0s.");
            }

            Context.Add(itemFile);
            Context.SaveChanges();

            return(Task.FromResult(true));
        }
Example #5
0
        /// <summary>
        /// 设置对应的视图行
        /// </summary>
        /// <param name="p_item"></param>
        /// <param name="p_isAsync">是否异步设置DataContext</param>
        public void SetItem(TvItem p_item, bool p_isAsync)
        {
            if (_row == p_item)
            {
                return;
            }

            // 虚拟行时需要重置
            if (_row != null)
            {
                _row.ValueChanged = null;
            }

            _row = p_item;
            if (_row != null)
            {
                // 值变化时通过切换DataContext更新
                _row.ValueChanged = OnValueChanged;
                SetIndent(_row.Depth * _owner.Indent);
            }

            if (p_isAsync)
            {
                SetDataContextAsync();
            }
            else
            {
                DataContext = _row;
            }
        }
Example #6
0
        public Task <bool> UpdateTvItemAsync(TvItem item)
        {
            Context.Update(item);
            Context.SaveChanges();

            return(Task.FromResult(true));
        }
Example #7
0
 public TvPanelItem(Tv p_owner, TvItem p_row)
 {
     _owner  = p_owner;
     _row    = p_row;
     _indent = _row.Depth * _owner.Indent;
     LoadContent();
     DataContext = _row;
 }
Example #8
0
        public async Task <IActionResult> DetailsAnonymous(int id)
        {
            TvItem item = await _tvItemService.FetchTvItemAsync(id, true);

            await _eventService.AddWebServerLogAsync(HttpContext.Connection.RemoteIpAddress.ToString(), WebServerLogType.AnonymousDetails, "", id);

            return(View(item));
        }
Example #9
0
        public Task <bool> SaveImageFilesAsync(TvItem item, List <IFormFile> modelFiles)
        {
            foreach (IFormFile formFile in modelFiles)
            {
                Image <Rgba32> image       = null;
                Stream         inputStream = formFile.OpenReadStream();

                image = Image.Load(inputStream);

                double height = image.Height;
                double width  = image.Width;
                double k1     = width / Constants.MAX_IMAGE_WIDTH;
                double k2     = height / Constants.MAX_IMAGE_HEIGHT;
                if (k1 > k2 && k1 > 1)
                {
                    image.Mutate(x => x.Resize(Constants.MAX_IMAGE_WIDTH, (int)(height / k1)));
                }

                if (k1 < k2 && k2 > 1)
                {
                    image.Mutate(x => x.Resize((int)(width / k2), Constants.MAX_IMAGE_HEIGHT));
                }

                string     extension = Path.GetExtension(formFile.FileName) ?? "";
                string     filename  = item.ID + "_" + Guid.NewGuid() + extension;
                TvItemFile itemFile  = new TvItemFile()
                {
                    FileName = filename,
                    Length   = formFile.Length,
                    TvItemId = item.ID
                };

                using (var fileStream = new FileStream(itemFile.AbsolutePath, FileMode.Create))
                {
                    if (extension.ToLower().EndsWith("jpg") || extension.ToLower().EndsWith("jpeg"))
                    {
                        image.SaveAsJpeg(fileStream);
                    }
                    else if (extension.ToLower().EndsWith("png"))
                    {
                        image.SaveAsPng(fileStream);
                    }
                    else
                    {
                        throw new Exception($"Unsupported image file extension [{extension}].");
                    }

                    itemFile.Length = fileStream.Length;
                    Context.Add(itemFile);
                }
            }

            Context.SaveChanges();

            return(Task.FromResult(true));
        }
Example #10
0
        public static string GetFormattedDuration(TvItem item)
        {
            long duration = item.Duration * item.Files.Count;

            TimeSpan time = TimeSpan.FromSeconds(duration);

            //here backslash is must to tell that colon is
            //not the part of format, it just a character that we want in output
            return(time.ToString(@"mm\:ss"));
        }
Example #11
0
 public static TextBlock Icon(TvItem p_item)
 {
     return(new TextBlock
     {
         Style = Res.LvTextBlock,
         FontFamily = Res.IconFont,
         TextAlignment = TextAlignment.Center,
         Text = (p_item.Children.Count > 0) ? "\uE067" : "\uE002",
     });
 }
Example #12
0
        /// <summary>
        /// 生成行内容
        /// </summary>
        /// <param name="p_item"></param>
        /// <returns></returns>
        TvPanelItem CreateVirRow(TvItem p_item)
        {
            var row = new TvPanelItem(_owner);

            if (p_item != null)
            {
                row.SetItem(p_item, false);
            }
            return(row);
        }
Example #13
0
 public TvItemEditViewModel(TvItem item) : this()
 {
     TvItem = item;
     LocationCheckboxes.LocationBanskaBystrica =
         item.Locations.Any(x => x.Location == Location.BanskaBystrica);
     LocationCheckboxes.LocationKosice =
         item.Locations.Any(x => x.Location == Location.Kosice);
     LocationCheckboxes.LocationZilina =
         item.Locations.Any(x => x.Location == Location.Zilina);
 }
Example #14
0
        public static long GetTotalFileSizeLong(TvItem item)
        {
            long size = 0;

            foreach (TvItemFile itemFile in item.Files)
            {
                size += itemFile.Deleted ? 0 : itemFile.Length;
            }

            return(size);
        }
Example #15
0
        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            TvItem item = (TvItem)value;

            if (DateTime.Compare(item.StartTime, item.EndTime) < 0)
            {
                return(ValidationResult.Success);
            }

            return(new ValidationResult(base.ErrorMessageString));
        }
Example #16
0
 void AddChildRows(TvItem p_parent)
 {
     if (p_parent.Children.Count > 0)
     {
         foreach (var item in p_parent.Children)
         {
             Children.Add(new TvPanelItem(_owner, item));
             AddChildRows(item);
         }
     }
 }
Example #17
0
        /// <summary>
        /// 刷新视图列表
        /// </summary>
        public void Refresh()
        {
            var rootData = _data.GetTreeRoot();

            if (rootData == null && _owner.FixedRoot == null)
            {
                _owner.ClearItems();
                return;
            }

            var rootItems = _owner.RootItems;

            rootItems.Clear();
            if (_owner.FixedRoot != null)
            {
                // 固定根节点
                TvItem fixedItem = new TvItem(_owner, _owner.FixedRoot, null);
                foreach (var item in rootData)
                {
                    TvItem ti = new TvItem(_owner, item, fixedItem);
                    BuildChildren(ti);
                    fixedItem.Children.Add(ti);
                }
                // 根节点状态
                if (fixedItem.Children.Count > 0 || _owner.IsDynamicLoading)
                {
                    fixedItem.ExpandedState = TvItemExpandedState.NotExpanded;
                }
                else
                {
                    fixedItem.ExpandedState = TvItemExpandedState.Hide;
                }
                rootItems.Add(fixedItem);
            }
            else
            {
                foreach (var item in rootData)
                {
                    TvItem ti = new TvItem(_owner, item, null);
                    BuildChildren(ti);
                    rootItems.Add(ti);
                }
            }

            // 自动展开第一个节点
            if (!_owner.IsDynamicLoading && rootItems[0].Children.Count > 0)
            {
                rootItems[0].IsExpanded = true;
            }
            _owner.LoadItems();
        }
Example #18
0
        public async Task <IActionResult> Edit([Bind] TvItemEditViewModel model)
        {
            TvItem item = await _tvItemService.FetchTvItemAsync(model.TvItem.ID);

            if (ModelState.IsValid)
            {
                // update item
                item.Title     = model.TvItem.Title;
                item.StartTime = model.TvItem.StartTime;
                item.EndTime   = model.TvItem.EndTime;
                item.Duration  = model.TvItem.Duration;

                // update locations
                item.Locations = new List <TvItemLocation>();
                if (model.LocationCheckboxes.LocationBanskaBystrica)
                {
                    item.Locations.Add(new TvItemLocation()
                    {
                        TvItemId = item.ID, Location = Location.BanskaBystrica
                    });
                }

                if (model.LocationCheckboxes.LocationKosice)
                {
                    item.Locations.Add(new TvItemLocation()
                    {
                        TvItemId = item.ID, Location = Location.Kosice
                    });
                }

                if (model.LocationCheckboxes.LocationZilina)
                {
                    item.Locations.Add(new TvItemLocation()
                    {
                        TvItemId = item.ID, Location = Location.Zilina
                    });
                }

                await _tvItemService.UpdateTvItemAsync(item);

                await _eventService.AddWebServerLogAsync(User.Identity.Name, WebServerLogType.ItemUpdate, "", item.ID);

                return(RedirectToAction(nameof(Index)));
            }

            // filter deleted files
            model.TvItem.Files = item.Files.Where(f => f.Deleted == false).ToList();

            return(View(model));
        }
Example #19
0
        public async Task <IActionResult> DeleteFile(int id)
        {
            TvItemFile itemFile = await _tvItemService.FetchTvItemFileAsync(id);

            TvItem item = await _tvItemService.FetchTvItemAsync(itemFile.TvItemId);

            await _tvItemService.DeleteTvItemFileAsync(id);

            await _eventService.AddWebServerLogAsync(
                User.Identity.Name,
                WebServerLogType.ItemDeleteSingleFile,
                $"User deleted file[{itemFile.FileName}] for item [{item.GetDetailHyperlink(false)}] with id [{item.ID}].",
                item.ID);

            return(RedirectToAction(nameof(Edit), new { id = item.ID }));
        }
Example #20
0
        public Task DeleteTvItemFilesAsync(int tvItemId)
        {
            TvItem tvItem = FetchTvItemAsync(tvItemId).Result;

            Console.WriteLine("deleting files: " + String.Join(", ", tvItem.Files.Select(x => x.FileName)));

            foreach (TvItemFile file in tvItem.Files)
            {
                _fileService.DeletePhysicalFileAsync(file.AbsolutePath);
                file.Deleted = true;
                Context.TvItemFile.Update(file);
            }

            Context.SaveChanges();

            return(Task.CompletedTask);
        }
Example #21
0
        internal static TvItem ToTvItem(this Dto.TvDetail.TvDetailDto tv)
        {
            var tvItem = new TvItem
            {
                Id              = tv.Id ?? 0,
                Overview        = tv.Overview,
                BackdropPath    = tv.BackdropPath,
                Title           = tv.Name,
                PosterPath      = tv.PosterPath,
                ReleaseDate     = tv.FirstAirDate,
                VoteAverage     = tv.VoteAverage ?? 0,
                NumberOfSeasons = tv.NumberOfSeasons ?? 0
            };

            tv.Genres.ForEach(genre => tvItem.Genres += genre.Name + " ");

            return(tvItem);
        }
Example #22
0
        public static string GetFilesList(TvItem item)
        {
            StringBuilder sb = new StringBuilder();

            foreach (TvItemFile itemFile in item.Files)
            {
                if (itemFile.Deleted)
                {
                    sb.Append(itemFile.FileName + "<br />");
                }
                else
                {
                    sb.Append("<a href=\"" + itemFile.Url + "\" target=\"_blank\">" + itemFile.FileName + "</a>&nbsp;&nbsp;" + GetFileSize(itemFile) + "<br />");
                }
            }

            return(sb.ToString());
        }
Example #23
0
        /// <summary>
        /// 递归加载子节点
        /// </summary>
        /// <param name="p_parent"></param>
        void BuildChildren(TvItem p_parent)
        {
            foreach (var item in _data.GetTreeItemChildren(p_parent.Data))
            {
                TvItem ti = new TvItem(_owner, item, p_parent);
                BuildChildren(ti);
                p_parent.Children.Add(ti);
            }

            // 节点状态
            if (p_parent.Children.Count > 0 || _owner.IsDynamicLoading)
            {
                p_parent.ExpandedState = TvItemExpandedState.NotExpanded;
            }
            else
            {
                p_parent.ExpandedState = TvItemExpandedState.Hide;
            }
        }
Example #24
0
        public static string GetStatusFormat(TvItem item)
        {
            if (DateTime.Compare(DateTime.Now, item.StartTime) < 0)
            {
                return("<span class=\"time-inactive\">INACTIVE</span><br /> " +
                       "starts in " + GetRemainingTime(item.StartTime, DateTime.Now) +
                       "<br />startTime: " + item.StartTime.ToString("dd.MM.yyyy HH:mm") +
                       "<br />endTime: " + item.EndTime.ToString("dd.MM.yyyy HH:mm"));
            }

            if (DateTime.Compare(item.StartTime, DateTime.Now) < 0 &&
                DateTime.Compare(DateTime.Now, item.EndTime) < 0)
            {
                return("<span class=\"time-active\">ACTIVE</span><br /> " +
                       "ends in " + GetRemainingTime(item.StartTime, item.EndTime) +
                       "<br />startTime: " + item.StartTime.ToString("dd.MM.yyyy HH:mm") +
                       "<br />endTime: " + item.EndTime.ToString("dd.MM.yyyy HH:mm"));;
            }

            return("<span class=\"time-expired\">EXPIRED</span>");
        }
Example #25
0
        public Task <bool> ReplaceVideoFileAsync(TvItem tvItem, IFormFile file)
        {
            string     filename    = tvItem.ID + "_" + Guid.NewGuid() + Path.GetExtension(file.FileName);
            TvItemFile newItemFile = new TvItemFile()
            {
                FileName = filename,
                Length   = file.Length
            };

            using (var fileStream = new FileStream(newItemFile.AbsolutePath, FileMode.Create))
            {
                file.CopyTo(fileStream);
            }

            if (tvItem.Duration == 0)
            {
                throw new Exception("Video duration is 0s.");
            }

            TvItemFile tvItemFile = tvItem.Files.FirstOrDefault();

            if (tvItemFile != null)
            {
                bool success = DeletePhysicalFileAsync(tvItemFile.FileName).Result;
                // if not successful delete, then new zombie file

                tvItemFile.FileName = newItemFile.FileName;
                tvItemFile.Length   = newItemFile.Length;

                Context.Update(tvItemFile);
                Context.Update(tvItem);
                Context.SaveChanges();

                return(Task.FromResult(true));
            }
            else
            {
                throw new Exception($"TvItem with id {tvItem.ID} does not have any files.");
            }
        }
Example #26
0
        public static string GetTotalFileSize(TvItem item)
        {
            long size = 0;

            foreach (TvItemFile itemFile in item.Files)
            {
                size += itemFile.Deleted ? 0 : itemFile.Length;
            }

            string[] sizes = { "B", "KB", "MB", "GB", "TB" };
            int      order = 0;

            while (size >= 1024 && order < sizes.Length - 1)
            {
                order++;
                size = size / 1024;
            }

            // Adjust the format string to your preferences. For example "{0:0.#}{1}" would
            // show a single decimal place, and no space.
            return(String.Format("{0:0.##} {1}", size, sizes[order]));
        }
Example #27
0
        public static TextBlock RowColor(TvItem p_item)
        {
            var tb = new TextBlock
            {
                Style = Res.LvTextBlock,
                Text  = $" ({p_item.Row.Str("code")})",
            };
            string code = p_item.Row.Str("code");

            if (code.Length < 4)
            {
                p_item.Foreground = Res.RedBrush;
            }
            else if (code.Length > 4)
            {
                p_item.Foreground = Res.GreenBrush;
            }

            if (p_item.Children.Count > 4)
            {
                p_item.Background = Res.浅黄背景;
            }
            return(tb);
        }
 public TvDetailViewModel(TvItem tvItem)
 {
     this.Tv = tvItem;
 }
Example #29
0
        /// <summary>
        /// 从根节点展开到当前节点,并滚动到可视范围
        /// </summary>
        /// <param name="p_item"></param>
        internal void ScrollIntoItem(TvItem p_item)
        {
            if (p_item == null)
            {
                return;
            }

            p_item.ExpandAll();
            UpdateLayout();

            var scroll = _owner.Scroll;

            if (scroll.ScrollableHeight == 0)
            {
                return;
            }

            if (_owner.IsInnerScroll)
            {
                // 内置滚动栏时
                double scrollBottom = scroll.VerticalOffset + scroll.ViewportHeight;
                if (_owner.IsVirtualized)
                {
                    double top = _owner.RootItems.GetVerIndex(p_item) * _rowHeight;
                    if (top < scroll.VerticalOffset)
                    {
                        scroll.ChangeView(null, top, null);
                    }
                    else if (top + _rowHeight > scrollBottom)
                    {
                        scroll.ChangeView(null, scroll.VerticalOffset + (top + _rowHeight - scrollBottom), null);
                    }
                }
                else
                {
                    Rect rc = _owner.RootItems.GetExpandedPostion(p_item, this);
                    if (rc.Top < scroll.VerticalOffset)
                    {
                        scroll.ChangeView(null, rc.Top, null);
                    }
                    else if (rc.Bottom > scrollBottom)
                    {
                        scroll.ChangeView(null, scroll.VerticalOffset + (rc.Bottom - scrollBottom), null);
                    }
                }
            }
            else
            {
                // 滚动栏在外部
                var pt = TransformToVisual(_owner.Scroll).TransformPoint(new Point());
                if (_owner.IsVirtualized)
                {
                    double top = _owner.RootItems.GetVerIndex(p_item) * _rowHeight + pt.Y + scroll.VerticalOffset;
                    if (top < scroll.VerticalOffset)
                    {
                        scroll.ChangeView(null, top, null);
                    }
                    else if (top + _rowHeight > scroll.ViewportHeight)
                    {
                        scroll.ChangeView(null, top + _rowHeight - scroll.ViewportHeight, null);
                    }
                }
                else
                {
                    Rect   rc  = _owner.RootItems.GetExpandedPostion(p_item, this);
                    double top = rc.Top + pt.Y + scroll.VerticalOffset;
                    if (top < scroll.VerticalOffset)
                    {
                        scroll.ChangeView(null, top, null);
                    }
                    else if (rc.Bottom + pt.Y + scroll.VerticalOffset > scroll.ViewportHeight)
                    {
                        scroll.ChangeView(null, scroll.VerticalOffset + rc.Bottom + pt.Y - scroll.ViewportHeight, null);
                    }
                }
            }
        }
Example #30
0
        void ArrangeVirRows(Size p_finalSize)
        {
            double deltaY = 0;

            if (!_owner.IsInnerScroll)
            {
                // 面板与ScrollViewer的相对距离,以滚动栏为参照物,面板在右下方时为正数
#if UWP
                if (_owner.Scroll.ActualHeight > 0)
                {
                    // 当切换win时,再次显示Scroll时ActualHeight为0,计算相对位置错误!采用切换前的相对位置
                    var pt = TransformToVisual(_owner.Scroll).TransformPoint(new Point());
                    deltaY = pt.Y;
                }
#else
                // uno中面板与Scroll的相对距离始终为滚动栏未移动时之间的距离!
                var pt = TransformToVisual(_owner.Scroll).TransformPoint(new Point());
                deltaY = pt.Y - _owner.Scroll.VerticalOffset;
#endif
            }
            else
            {
                // 内置滚动栏时,垂直距离始终 <= 0
                deltaY = -_owner.Scroll.VerticalOffset;
            }

            // 无数据时,也要重新布局
            if (_owner.RootItems.Count == 0 ||
                _rowHeight == 0 ||
                deltaY >= _maxSize.Height ||       // 面板在滚动栏下方外部
                deltaY <= -p_finalSize.Height)     // 面板在滚动栏上方外部
            {
                foreach (var elem in Children)
                {
                    ((UIElement)elem).Arrange(_rcEmpty);
                }
                return;
            }

            IEnumerator <TvItem> tvItems;
            bool hasNext = true;

            // 面板可见,在滚动栏下方,按正常顺序布局
            if (deltaY >= 0 && deltaY < _maxSize.Height)
            {
                int iDataRow = Children.Count;
                tvItems = _owner.RootItems.GetExpandedItems().GetEnumerator();
                for (int i = 0; i < Children.Count; i++)
                {
                    var    item = (TvPanelItem)Children[i];
                    double top  = i * _rowHeight;
                    if (hasNext)
                    {
                        hasNext = tvItems.MoveNext();
                    }

                    // 数据行已结束 或 剩下行不可见,结束布局
                    if (deltaY + top > _maxSize.Height || !hasNext)
                    {
                        iDataRow = i;
                        break;
                    }

                    // 布局虚拟行
                    item.SetItem(tvItems.Current, true);
                    item.Arrange(new Rect(0, top, p_finalSize.Width, _rowHeight));
                }
                tvItems.Dispose();

                // 将剩余的虚拟行布局到空区域
                if (iDataRow < Children.Count)
                {
                    for (int i = iDataRow; i < Children.Count; i++)
                    {
                        ((UIElement)Children[i]).Arrange(_rcEmpty);
                    }
                }
                return;
            }

            // deltaY < 0 && deltaY > -p_finalSize.Height
            // 面板顶部超出滚动栏 并且 没有整个面板都超出,此时deltaY为负数

            // 页面偏移量
            double offset = deltaY % _pageHeight;
            // 最顶部的数据行索引
            int iRow = (int)Math.Floor(-deltaY / _rowHeight);
            // 最顶部的虚拟行索引
            int iVirRow = iRow % Children.Count;
            // 页面顶部偏移
            double deltaTop = -deltaY + offset;
            // 跳过不显示的节点
            tvItems = _owner.RootItems.GetExpandedItems().Skip(iRow).GetEnumerator();

            for (int i = 0; i < Children.Count; i++)
            {
                var item = (TvPanelItem)Children[iVirRow];
                if (hasNext)
                {
                    hasNext = tvItems.MoveNext();
                }
                if (hasNext)
                {
                    // 布局虚拟行
                    TvItem ti  = tvItems.Current;
                    double top = iVirRow * _rowHeight + deltaTop;
                    item.SetItem(ti, true);
                    item.Arrange(new Rect(0, top, p_finalSize.Width, _rowHeight));
                }
                else
                {
                    // 多余的行布局在外部
                    item.Arrange(_rcEmpty);
                }

                iVirRow++;
                if (iVirRow >= Children.Count)
                {
                    // 虚拟行放入下页
                    deltaTop += _pageHeight;
                    iVirRow   = 0;
                }
            }
            tvItems.Dispose();
        }