UWP开发之StreamSocket聊天室(三)
来源:互联网 发布:初中课本同步软件 编辑:程序博客网 时间:2024/05/19 16:06
本节知识点:
- SplitView做导航菜单
- MvvmLight 的 SimpleIoc、ServiceLocator 的使用
- MvvmLight 的Messenger消息通知机制的使用
- MvvmLight 中DispatcherHelper 的使用
这里我们会使用MVVMLight框架,任何接触过 WindowsPhone 、Win 8.1 开发的 工作人员都知道在日常开发中我们会使用MVVM设计模式进行依赖关系解耦,而MVVMLight无疑是MVVM设计模式上的最优秀、使用最广泛的一个框架。
首先在项目中创建一个Pages文件夹,新建两个页面:ClientMessage.xaml(聊天页面)、ClientSetting.xaml(设置页面),创建好之后先不用写任何代码,因为我们还需要做个导航,我们就使用系统自己创建的MainPage页面来做个导航。
一、MainPage的实现
MianPage.xaml中我们来使用SplitView和Frame控件来做导航和主页面(SplitView菜单的展开折叠按钮没有做,有想加上的朋友自己加个按钮即可),前台xaml主代码如下:
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> <SplitView DisplayMode="CompactInline" CompactPaneLength="64" > <SplitView.Pane> <Grid RequestedTheme="Dark"> <controls:NavMenuListView x:Name="NavListView" ContainerContentChanging="NavMenuItemContainerContentChanging" ItemInvoked="NavMenuList_ItemInvoked" ItemContainerStyle="{StaticResource ListViewItemBaseStyle}" ItemsSource="{x:Bind NavList}" Background="#FF1C3048"> <controls:NavMenuListView.ItemTemplate> <DataTemplate x:DataType="models:NavModel"> <Grid Margin="-12,0,0,0" Height="64"> <Grid.ColumnDefinitions> <ColumnDefinition Width="64"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Grid Width="24" Height="24"> <SymbolIcon ToolTipService.ToolTip="{x:Bind Title}" Symbol="{x:Bind Icon}"/> </Grid> <TextBlock Margin="16,0,1,0" Grid.Column="1" Text="{x:Bind Title}" VerticalAlignment="Center" /> </Grid> </DataTemplate> </controls:NavMenuListView.ItemTemplate> </controls:NavMenuListView> </Grid> </SplitView.Pane> <Grid> <Frame x:Name="MainPageFrame" /> </Grid> </SplitView></Grid>
上面的代码中,我们在SplitView控件的Pane元素中使用了一个自定义控件NavMenuListView,这个控件继承与ListView类,创建一个Controls文件夹,新建一个NavMenuList.cs文件,定义一个NavMenuListView类继承与ListView,具体代码如下:
public class NavMenuListView : ListView{ private SplitView _splitViewHost; public NavMenuListView() { SelectionMode = ListViewSelectionMode.Single; IsItemClickEnabled = true; ItemClick += ItemClickedHandler; // Locate the hosting SplitView control Loaded += (s, a) => { var parent = VisualTreeHelper.GetParent(this); while (parent != null && !(parent is SplitView)) { parent = VisualTreeHelper.GetParent(parent); } if (parent != null) { _splitViewHost = parent as SplitView; _splitViewHost.RegisterPropertyChangedCallback(SplitView.IsPaneOpenProperty, (sender, args) => { OnPaneToggled(); }); // Call once to ensure we're in the correct state OnPaneToggled(); } }; } protected override void OnApplyTemplate() { base.OnApplyTemplate(); // Remove the entrance animation on the item containers. for (var i = 0; i < ItemContainerTransitions.Count; i++) { if (ItemContainerTransitions[i] is EntranceThemeTransition) { ItemContainerTransitions.RemoveAt(i); } } } /// <summary> /// Mark the <paramref name="item" /> as selected and ensures everything else is not. /// If the <paramref name="item" /> is null then everything is unselected. /// </summary> /// <param name="item"></param> public void SetSelectedItem(ListViewItem item) { var index = -1; if (item != null) { index = IndexFromContainer(item); } for (var i = 0; i < Items.Count; i++) { var lvi = (ListViewItem) ContainerFromIndex(i); if (i != index) { lvi.IsSelected = false; } else if (i == index) { lvi.IsSelected = true; } } } /// <summary> /// Occurs when an item has been selected /// </summary> public event EventHandler<ListViewItem> ItemInvoked; /// <summary> /// Custom keyboarding logic to enable movement via the arrow keys without triggering selection /// until a 'Space' or 'Enter' key is pressed. /// </summary> /// <param name="e"></param> protected override void OnKeyDown(KeyRoutedEventArgs e) { var focusedItem = FocusManager.GetFocusedElement(); switch (e.Key) { case VirtualKey.Up: TryMoveFocus(FocusNavigationDirection.Up); e.Handled = true; break; case VirtualKey.Down: TryMoveFocus(FocusNavigationDirection.Down); e.Handled = true; break; case VirtualKey.Tab: var shiftKeyState = CoreWindow.GetForCurrentThread().GetKeyState(VirtualKey.Shift); var shiftKeyDown = (shiftKeyState & CoreVirtualKeyStates.Down) == CoreVirtualKeyStates.Down; // If we're on the header item then this will be null and we'll still get the default behavior. if (focusedItem is ListViewItem) { var currentItem = (ListViewItem) focusedItem; var onlastitem = currentItem != null && IndexFromContainer(currentItem) == Items.Count - 1; var onfirstitem = currentItem != null && IndexFromContainer(currentItem) == 0; if (!shiftKeyDown) { if (onlastitem) { TryMoveFocus(FocusNavigationDirection.Next); } else { TryMoveFocus(FocusNavigationDirection.Down); } } else // Shift + Tab { if (onfirstitem) { TryMoveFocus(FocusNavigationDirection.Previous); } else { TryMoveFocus(FocusNavigationDirection.Up); } } } else if (focusedItem is Control) { if (!shiftKeyDown) { TryMoveFocus(FocusNavigationDirection.Down); } else // Shift + Tab { TryMoveFocus(FocusNavigationDirection.Up); } } e.Handled = true; break; case VirtualKey.Space: case VirtualKey.Enter: // Fire our event using the item with current keyboard focus InvokeItem(focusedItem); e.Handled = true; break; default: base.OnKeyDown(e); break; } } /// <summary> /// This method is a work-around until the bug in FocusManager.TryMoveFocus is fixed. /// </summary> /// <param name="direction"></param> private void TryMoveFocus(FocusNavigationDirection direction) { if (direction == FocusNavigationDirection.Next || direction == FocusNavigationDirection.Previous) { FocusManager.TryMoveFocus(direction); } else { var control = FocusManager.FindNextFocusableElement(direction) as Control; if (control != null) { control.Focus(FocusState.Programmatic); } } } private void ItemClickedHandler(object sender, ItemClickEventArgs e) { // Triggered when the item is selected using something other than a keyboard var item = ContainerFromItem(e.ClickedItem); InvokeItem(item); } private void InvokeItem(object focusedItem) { SetSelectedItem(focusedItem as ListViewItem); ItemInvoked(this, focusedItem as ListViewItem); if (_splitViewHost.IsPaneOpen && ( _splitViewHost.DisplayMode == SplitViewDisplayMode.CompactOverlay || _splitViewHost.DisplayMode == SplitViewDisplayMode.Overlay)) { _splitViewHost.IsPaneOpen = false; if (focusedItem is ListViewItem) { ((ListViewItem) focusedItem).Focus(FocusState.Programmatic); } } } /// <summary> /// Re-size the ListView's Panel when the SplitView is compact so the items /// will fit within the visible space and correctly display a keyboard focus rect. /// </summary> private void OnPaneToggled() { if (_splitViewHost.IsPaneOpen) { ItemsPanelRoot.ClearValue(WidthProperty); ItemsPanelRoot.ClearValue(HorizontalAlignmentProperty); } else if (_splitViewHost.DisplayMode == SplitViewDisplayMode.CompactInline || _splitViewHost.DisplayMode == SplitViewDisplayMode.CompactOverlay) { ItemsPanelRoot.SetValue(WidthProperty, _splitViewHost.CompactPaneLength); ItemsPanelRoot.SetValue(HorizontalAlignmentProperty, HorizontalAlignment.Left); } }}
MainPage页面中的自定义NavMenuListView控件还是设置了ItemContainerStyle样式,样式我写在了一个名叫MainPageStyle.xaml样式文件中,样式文件在App.xaml中添加了引用如下:
<Application.Resources> <ResourceDictionary> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="Style/MainPageStyle.xaml"/> </ResourceDictionary.MergedDictionaries> </ResourceDictionary></Application.Resources>
这样引用在App.xaml里,方便全局使用该样式文件里定义的样式资源。
Ok,在MainPageStyle.xaml样式文件中定义我们需要的ItemContainerStyle样式资源,打开MainPageStyle.xaml文件编写如下代码:
<Color x:Key="MainColor" >#25C4A4</Color> <SolidColorBrush x:Key="MainThemeBrush" Color="#FF25C4A4"/><Style x:Key="ListViewItemBaseStyle" TargetType="ListViewItem"> <Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}"/> <Setter Property="FontSize" Value="{ThemeResource ControlContentThemeFontSize}"/> <Setter Property="Background" Value="Transparent"/> <Setter Property="Foreground" Value="{ThemeResource SystemControlForegroundBaseHighBrush}"/> <Setter Property="TabNavigation" Value="Local"/> <Setter Property="IsHoldingEnabled" Value="True"/> <!--<Setter Property="Padding" Value="12,0,12,0"/>--> <Setter Property="HorizontalContentAlignment" Value="Left"/> <Setter Property="VerticalContentAlignment" Value="Center"/> <Setter Property="MinWidth" Value="{ThemeResource ListViewItemMinWidth}"/> <Setter Property="MinHeight" Value="{ThemeResource ListViewItemMinHeight}"/> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="ListViewItem"> <ListViewItemPresenter CheckBrush="{ThemeResource SystemControlForegroundBaseMediumHighBrush}" ContentMargin="{TemplateBinding Padding}" CheckMode="Inline" ContentTransitions="{TemplateBinding ContentTransitions}" CheckBoxBrush="{ThemeResource SystemControlForegroundBaseMediumHighBrush}" DragForeground="{ThemeResource ListViewItemDragForegroundThemeBrush}" DragOpacity="{ThemeResource ListViewItemDragThemeOpacity}" DragBackground="{ThemeResource ListViewItemDragBackgroundThemeBrush}" DisabledOpacity="{ThemeResource ListViewItemDisabledThemeOpacity}" FocusBorderBrush="{ThemeResource SystemControlForegroundAltHighBrush}" FocusSecondaryBorderBrush="{ThemeResource SystemControlForegroundBaseHighBrush}" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" PointerOverForeground="{ThemeResource SystemControlHighlightAltBaseHighBrush}" PressedBackground="{ThemeResource SystemControlHighlightListMediumBrush}" PlaceholderBackground="{ThemeResource ListViewItemPlaceholderBackgroundThemeBrush}" PointerOverBackground="{ThemeResource SystemControlHighlightListLowBrush}" ReorderHintOffset="{ThemeResource ListViewItemReorderHintThemeOffset}" SelectedPressedBackground="{StaticResource MainThemeBrush}" SelectionCheckMarkVisualEnabled="True" SelectedForeground="{ThemeResource SystemControlHighlightAltBaseHighBrush}" SelectedPointerOverBackground="{StaticResource MainThemeBrush}" SelectedBackground="{StaticResource MainThemeBrush}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"/> </ControlTemplate> </Setter.Value> </Setter> </Style>
样式中主要设置了ListView的项选中后的背景色以及鼠标经过的背景色等,定义好之后MainPage页面中的控件就可以使用它了。
在自定义的菜单导航NavMenuListView中,ItemsSource数据源我们绑定到了一个NavList集合以及订阅了ContainerContentChanging、ItemInvoked的两个事件,我们来看下完整的MainPage.xaml.cs代码:
public sealed partial class MainPage : Page{ public static Frame MainFrame { get; set; } public List<NavModel> NavList = new List<NavModel> { new NavModel {Icon = Symbol.Message,PageType = typeof(ClientMessage),Title = "消息"}, new NavModel {Icon = Symbol.Setting,PageType = typeof(ClientSetting),Title = "设置"} }; public MainPage() { this.InitializeComponent(); MainFrame = MainPageFrame; } private void NavMenuItemContainerContentChanging(ListViewBase sender, ContainerContentChangingEventArgs args) { if (!args.InRecycleQueue && args.Item is NavModel) { args.ItemContainer.SetValue(AutomationProperties.NameProperty, ((NavModel)args.Item).Title); } else { args.ItemContainer.ClearValue(AutomationProperties.NameProperty); } } private void NavMenuList_ItemInvoked(object sender, ListViewItem e) { var item = (NavModel)((NavMenuListView)sender).ItemFromContainer(e); if (item?.PageType != null && item.PageType != typeof(object) && item.PageType != MainFrame.CurrentSourcePageType) { MainFrame.Navigate(item.PageType); } }}
代码很简单就不解释了,主要就是点击导航到某个界面。导航Model记得添加Models项目的引用。
二、安装MvvmLight框架
接下来我们来安装下MVVMLight的框架,右键"引用"点击"管理NuGet程序包",在搜索框中输入"mvvmlight"进行搜索,待搜索结果出来后选择MVVMLight包点击安装进行安装(已安装的显示为卸载按钮):
三、ViewModelLocator的创建
安装好MvvmLight后,我们新建一个ViewModel文件夹,新建一个名为ViewModelLocator.cs的类用来统一管理定位我们的ViewModel,打开这个文件编写代码如下:
public class ViewModelLocator{ private static ViewModelLocator _default; public ViewModelLocator() { ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default); //在Ioc容器里面注册每个VM SimpleIoc.Default.Register<SettingPageViewModel>(); SimpleIoc.Default.Register<MessagePageViewModel>(); } /// <summary> /// 默认的全局ViewModelLocator实例,在App资源中声明 /// </summary> public static ViewModelLocator Default { get { if (_default != null) return _default; _default = Application.Current.Resources["locator"] as ViewModelLocator; if (_default == null) throw new NotImplementedException("App资源中没有声明ViewModelLocator"); return _default; } } /// <summary> /// 提供给外部的SettingPageViewModel VM /// </summary> public SettingPageViewModel SettingPageViewModel => ServiceLocator.Current.GetInstance<SettingPageViewModel>(); /// <summary> /// 提供给外部的MessagePageViewModel VM /// </summary> public MessagePageViewModel MessagePageViewModel => ServiceLocator.Current.GetInstance<MessagePageViewModel>();}
这里我们使用了MvvmLight框架提供的SImpleIoc和ServiceLocator类,SimpleIoc是什么?看到Ioc想必大家就明白了,它就是一个控制反转的容器,是将VM与使用者之间解耦的一个设计模式,这里对于IoC设计模式就不再做赘述,有兴趣的可以到这里了解下IoC模式:IoC模式
回到我们的项目中,由于View中肯定会使用到VM,所以我们在ViewModelLocator中使用SimpleIoc.Default.Register方法注册我们会用到的VM,而使用VM的地方我们就可以使用ServiceLocator.Current.GetInstance方法来获取到指定的VM对象。
为了简化访问VM的写法,我们定义了一个静态ViewModelLocator对象Default以及声明了各个VM为属性,在访问VM的时候就可以直接使用ViewModelLocator.Default.VM来直接使用。
ViewModelLocator中的Default属性是直接在App.xaml中获取的资源对象,所以我们需要在App.xaml中声明ViewModelLocator对象,让ViewModelLocator在程序运行时就被创建,完整的App.xaml代码如下:
<Application x:Class="SocketClientSample.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:SocketClientSample" xmlns:viewModel="using:SocketClientSample.ViewModel" RequestedTheme="Light"> <Application.Resources> <ResourceDictionary> <viewModel:ViewModelLocator x:Key="locator"/> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="Style/MainPageStyle.xaml"/> </ResourceDictionary.MergedDictionaries> </ResourceDictionary> </Application.Resources> </Application>
四、SettingPageViewModel的实现
我们在ViewModel文件夹中新建一个SettingPageViewModel.cs类用来处理设置界面里的逻辑,作为客户端,设置界面里我们需要让用户输入一个远程服务端的IP、端口号、聊天昵称等一些远程服务和聊天设置的一些功能。
先贴一下SettingPageViewModel的完整代码:
public class SettingPageViewModel : ViewModelBase{ private string _socketStateTxt = "未连接"; /// <summary> /// 监听状态文本描述 /// </summary> public string SocketStateTxt { get { return _socketStateTxt; } set { _socketStateTxt = value; RaisePropertyChanged(); } } /// <summary> /// Socket服务端 /// </summary> public SocketBase ClientSocket { get; set; } /// <summary> /// 用户信息 /// </summary> public UserModel UserModel { get; set; } = new UserModel(); /// <summary> /// 远程服务ip /// </summary> public string ServicerIp { get; set; } /// <summary> /// 端口号 /// </summary> public string ServicerPort { get; set; } /// <summary> /// 连接按钮点击事件 /// </summary> public async void ConnectionToServicer() { if (string.IsNullOrEmpty(UserModel.UserName) || string.IsNullOrEmpty(ServicerIp) || string.IsNullOrEmpty(ServicerPort)) return; //创建一个客户端Socket对象 ClientSocket = SocketFactory.CreatInkSocket(false, ServicerIp, ServicerPort); //当新消息到达时的行为 ClientSocket.MsgReceivedAction += data => { Messenger.Default.Send(data, "MsgReceivedAction"); }; //连接成功时的行为 ClientSocket.OnStartSuccess += () => { DispatcherHelper.CheckBeginInvokeOnUI(() => SocketStateTxt = "已连接"); }; //连接失败时的行为 ClientSocket.OnStartFailed += exc => { DispatcherHelper.CheckBeginInvokeOnUI(() => SocketStateTxt = $"断开的连接:{exc.Message}"); }; //开始连接远程服务端 await ClientSocket.Start(); }}
在ConnectionToServicer方法里,通过SocketFactory工厂来生成客户端StreamSocket对象,然后订阅接收到消息的MsgReceivedAction,在MsgReceivedAction方法里,一旦客户端接受到消息,就会使用MvvmLight框架提供的消息机制向App广播出去一条"MsgReceivedAction"消息(MvvmLight的消息机制,如有不懂得请先查看:Messenger and View Services in MVVM)。然后谁去注册该消息,具体 的怎么实现该消息的处理逻辑就和SettingPageViewModel没有任何关系了,这样做对解耦有很大的帮助,如果不使用MvvmLight框架的话这里处理着就比较麻烦了,因为SettingPageViewModel类中并不知道该怎么处理新消息,它也不拥有消息集合,即使知道需要将新消息填充到消息集合中,真的做起来,要么自己会暴露一个新消息到达的事件供外部订阅,要么自己会引用消息集合所在的类中来完成新消息添加到消息记录集合中的操作,很麻烦不说,VM之间也会产生很多依赖关系,这样不好。
上面代码中我们还订阅了ClientSocket的OnStartSuccess和OnStartFailed两个行为,当这两个行为任何一个被触发时就代表着连接服务器的状态发生了改变,那么我们就要改变一下SocketStateTxt属性的值,而由于我们是在异步的Action中要修改该UI属性,所以我们就等于在后台进程中访问UI线程,这里我们就需借助MvvmLight提供的DispatcherHelper类来帮助我们能搞访问UI线程。
DispatcherHelper类是专门用来处理跨UI线程的工具类,如果不使用它我们就要使用类似下面这种代码来处理辅助线程对UI线程的访问
Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => { // you code });
而要使用Dispatcher对象就必须拿到具体的页面Page对象,这样的话VM与Page之间就产生了依赖关系,这样就增加了耦合度,不好!
需要注意的是DispatcherHelper类并不是我们想要使用就能使用的,我们还需要在UI线程中初始化一下DispatcherHelper类,这样DispatcherHelper类就能正确的在UI主线程中随心所欲的访问UI线程,ok,那我们就在App.xaml.cs的OnLaunched方法中调用下DispatcherHelper的Initialize方法,代码如下:
DispatcherHelper.Initialize();
更多的DispatcherHelper的使用方法这里也不再赘述了,想了解的请点击:MVVM 应用程序中的多线程与调度
五、MessagePageViewModel的实现
MessagePageViewModel的逻辑也很简单,主要包含:
- 本地聊天记录MessagePageViewModel
- 要发送的文本TextMsg
- 客户端接受到消息后的逻辑MsgReceivedAction方法
- 发送消息的方法Send
- 聊天输入框按键抬起的事件逻辑MsgTextBoxKeyUp
代码如下:
public class MessagePageViewModel : ViewModelBase{ /// <summary> /// 本地聊天消息结合 /// </summary> public ObservableCollection<MessageModel> MessageCollection { get; set; } = new ObservableCollection<MessageModel>(); private string _textMsg; /// <summary> /// 要发送的文本 /// </summary> public string TextMsg { get { return _textMsg; } set { _textMsg = value; RaisePropertyChanged(); } } public MessagePageViewModel() { //注册 MsgReceivedAction 的 Message Messenger.Default.Register<MessageModel>(this, "MsgReceivedAction", MsgReceivedAction); } /// <summary> /// 发送聊天消息 /// </summary> /// <returns></returns> public async Task SendMsg() { var client = ViewModelLocator.Default.SettingPageViewModel.ClientSocket; if (!client.Working) return; //要发送的消息对象 var msg = new MessageModel { MessageType = MessageType.TextMessage, Message = TextMsg, SetDateTime = DateTime.Now, User = ViewModelLocator.Default.SettingPageViewModel.UserModel }; await client.SendMsg(msg); //发送完成后往本地的消息集合MessageCollection 添加一条刚发送的消息 msg.Horizontal = HorizontalAlignment.Right; MessageCollection.Add(msg); //发出 NewMsgAction Message Messenger.Default.Send(msg, "NewMsgAction"); TextMsg = null; } /// <summary> /// MsgReceivedAction Message 的具体逻辑 /// </summary> /// <param name="obj">接受到的消息数据</param> private void MsgReceivedAction(MessageModel obj) { //访问UI线程添加新聊天消息到本地聊天记录 DispatcherHelper.CheckBeginInvokeOnUI(() => { if (obj.MessageType == MessageType.Disconnect) ViewModelLocator.Default.SettingPageViewModel.ClientSocket.Dispose(); else MessageCollection.Add(obj); }); //发出 NewMsgAction Message Messenger.Default.Send(obj, "NewMsgAction"); } /// <summary> /// 输入框按键抬起事件 /// </summary> /// <param name="sender">触发者</param> /// <param name="key">按键数据</param> public async void MsgTextBoxKeyUp(object sender, KeyRoutedEventArgs key) { TextMsg = (sender as TextBox).Text; if (key.Key == VirtualKey.Enter) //如果按下Enter键 就发送聊天消息 { if (string.IsNullOrEmpty(TextMsg)) return; await SendMsg(); } }}
这里就可以看到在MessagePageViewModel的构造函数中订阅了客户端新消息到达的Message ->
MsgReceivedAction,当客户端ClientSocket对象接受到消息时发出该消息,具体的处理逻辑是由注册该消息的MessagePageViewModel来处理的。
而MessagePageViewModel中也对外发出了一个Message -> NewMsgAction,这个是为UI而发出的消息,逻辑是这样的:当服务端的新消息添加到本地聊天记录中后,通知UI端,该将滚动条滚动到聊天记录的最新的一条了。滚动代码会使用到具体的聊天记录控件ListView,所以我们用Messenger的消息机制解耦VM与Page之间的依赖关系是再好不过了。
好了,上面的MessagePageViewModel代码逻辑也很简单,注释很详细就不解释了。
前台的UI Page界面今天就不接着讲了,放到下篇博客吧……
本文出自:53078485群大咖Aran
0 0
- UWP开发之StreamSocket聊天室(三)
- UWP开发之StreamSocket聊天室 (一)
- UWP开发之StreamSocket聊天室(二)
- UWP开发之StreamSocket聊天室(四)
- UWP开发之StreamSocket聊天室(五)
- UWP之使用StreamSocket建立聊天室
- UWP之C++/CX开发
- uwp开发之 设置储存
- UWP开发入门系列笔记之(一):UWP初览
- Android开发之聊天室
- Win10的UWP开发之Hello World
- 【Win10】UAP/UWP/通用 开发之 SplitView
- UWP开发大坑之----路由事件
- UWP开发入门系列笔记之(零):UWP的前世今生
- java socket编程之聊天室(三)
- Win10开发之UWP控件的隐藏空间
- uwp开发之Windows.Data.Json解析json
- 新时尚Windows8开发(40):StreamSocket的使用
- Android利用JDBC连接服务器数据库
- ThinkPHP 实现将SESSION存 mysql db方式session
- static 与变量的关系
- Android编程规范不完全指南
- 程序设计语言的构成
- UWP开发之StreamSocket聊天室(三)
- 独乐乐不如众乐乐
- 欢迎使用CSDN-markdown编辑器
- Token bucket algorithm
- stackoverflow上Java相关回答技巧 测试demo
- Android之Input子系统事件分发流程
- Qstring将多个连续的空格替换成一个空格的方法
- Java中hashCode的作用
- 捍卫者移动存储介质管理系统