UWP开发之StreamSocket聊天室(三)


本节知识点:
  •     SplitView做导航菜单
  •     MvvmLight 的 SimpleIoc、ServiceLocator 的使用
  •     MvvmLight 的Messenger消息通知机制的使用
  •     MvvmLight 中DispatcherHelper 的使用
上回我们说到SocketBusiness 的创建以及实现,SocketBusiness保证了我们Socket网络通信的能力和App的底层处理逻辑,这篇我们开始介绍StreamSocket网络编程(聊天室-客户端)的前端代码的实现。

这里我们会使用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
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值