页面与ViewModel(下)
在上一篇博客中,笔者分享了一些从页面整体的角度对页面与ViewModel的思考。在本文中笔者希望从相对细节的角度分享一些对页面与ViewModel的思考。
比如,当我们在更新View Model中的绑定数据时,应该怎样更新呢?简单的自然可以用新的数据实例直接替代旧的,但是这样容易造成UI界面闪烁。尤其是绑定数据是一个列表的情况下,如果整个列表被替换,可以非常明显的看到列表"一闪"。这样的用户体验无疑是不理想的。那么我们在更新View Model中绑定的数据实例时,可以采用差异更新的方法。以一个数据列表为例,在更新时对比新旧列表,先遍历新表,对每一个元素查看在旧表中有无对应元素。如果没有,说明是新增的数据,可以将该新表中的元素同时加入到一个临时表和旧表中,如果旧表有排序则还需要注意插入的位置。如果有,说明是旧元素更新,则用新元素的值更新旧元素后,将旧元素加入到临时表中。然后遍历旧表,对旧表中每一个元素在临时表中查看有无对应元素。如果有,则不用做任何处理。如果没有,则说明该元素已经被删除,应该在旧表中将这个元素移除。这样对UI界面的更新看起来会比较平滑。
这里写一下笔者在旺信UWP中所写的差异化更新算法,权当抛砖引玉。
var bList = new List<bool>();//辅助列表 for (int j = 0; j < MainList.Count; j++)//辅助列表初始与旧表同长 { bList.Add(false); } for (int i = 0; i < groups.Count; i++)//遍历新表 { bool inserted = false; bool contains = false; for (int j = 0; j < MainList.Count; j++)//新表中的元素与旧表对比 { if (groups[i].key != MainList[j].key)//如果不是同一元素 { if ((groups[i].key == "群主")//尝试插入 || ( MainList[j].key != "群主" && MainList[j].key != "管理员" && (groups[i].key == "管理员" || groups[i].key.CompareTo(MainList[j].key) < 0) ) ) { MainList.Insert(j, groups[i]); bList.Insert(j, true); inserted = true; Debug.WriteLine("inserted:" + j + "," + groups[i].key); break; } } else//如果是同一元素,用新表元素内容更新旧表 { contains = true; MainList[j].update(groups[i]); bList[j] = true; break; } } if ((!contains) && (!inserted))//不包括在旧表内,也没有插入,则追加在旧表尾部 { MainList.Add(groups[i]); bList.Add(true); } } for (int i = bList.Count; i > 0; i--)//对比辅助列表,移除旧表中不应再存在的元素 { if (!bList[i-1]) { try { MainList.RemoveAt(i - 1); } catch (Exception) { Debug.WriteLine("RemoveAt error:" + i); } } }
在这段代码中,用新的数据groups更新旧的数据MainList。
再比如,在我们的页面上,我们一般都会放置一个表示正在加载数据的控件。这个加载中控件的状态一般也是绑定一个后台数据的。对于一般的页面,我们可以采取在加载数据前后设置该绑定值的方法来修改页面所显示的加载状态。而对于UWP旺信这种依赖网络,一个页面可能同时调用多个网络接口更新数据的情况,就不是非常合适了。比如a,b两个接口同时请求数据,将加载状态置为加载中。如果a接口先返回,则会将加载状态置为完成。而实际上b接口仍然在请求数据,正确的加载状态应该还是加载中,直到b接口也返回。为此笔者想到了可以增加一个初始值为0的计数变量,当有加载请求时就自增1,当请求异步结束或回调返回时就自减1,绑定的加载状态的get方法根据当计数是否为0返回是否在加载状态。这样一来就可以使多个加载请求都能正确的改变加载状态。
在旺信UWP中,笔者就为ViewModel添加了这样的变量:
public bool isLoading { get { return loadingCount > 0; } } private int _loadingCount = 0; public int loadingCount { get { return _loadingCount; } set { _loadingCount = (value < 0 ? 0 : value); RaisePropertyChanged("isLoading"); } }
在xaml页面上则将ProgressRing控件的IsActive属性绑定到isLoading变量上:
<ProgressRing Grid.Row="2" Grid.RowSpan="2" IsActive="{x:Bind thisData.isLoading,Mode=OneWay}" Width="60" Height="60" Foreground="{ThemeResource WXThemeColorBrush}"></ProgressRing>
在调用异步方法前后,并不直接设置isLoading变量,而是采取上面提到的,调用前loadingCount变量自增1,完成后loadingCount变量自减1的方法来影响ProgressRing控件所显示的加载状态IsActive。
另外,在使用x:Bind方法时,笔者发现如果把绑定image图片控件的source绑定到一个string,当绑定的string值为空时会在log中出现exception。即使在string值为空时把image控件隐藏也仍然会出现。然而旺信中数据的属性值基本都是从服务器传输到客户端的,有时确实会有一些图片的url为空。这样一来最好给图片属性值都给一个默认值。那么默认值该如何确定呢?如果是普通的占位图片,那么可能在不该出现图片的地方显示。经过实践,笔者选择了在应用中加入一个长度为0的图片,把该图片的uri作为图片属性的默认值。当然这个方法只是消除了log中的exception,具体是否提升了应用的效率,还有待验证。
以上就是笔者对对页面与ViewModel的细节的思考,希望能对小伙伴们开发UWP应用有所帮助。当然也欢迎大家拍砖,提出更多更好的经验,让我们共同进步。