[UWP]理解及擴展Expander

来源:http://www.cnblogs.com/dino623/archive/2017/09/13/Expander.html
-Advertisement-
Play Games

1. 前言 最近在自定義Expander的樣式,順便看了看它的源碼。 Expander控制項是一個ContentControl,它通過IsExpanded屬性或者通過點擊Header中的ToggleButton控制內容展開或隱藏。UWP SDK中沒提供這個控制項,而是在UWP Community Too ...


1. 前言

最近在自定義Expander的樣式,順便看了看它的源碼。
Expander控制項是一個ContentControl,它通過IsExpanded屬性或者通過點擊Header中的ToggleButton控制內容展開或隱藏。UWP SDK中沒提供這個控制項,而是在UWP Community Toolkit中 提供 。它是個教科書式的入門級控制項,代碼簡單,雖然仍然不盡如人意,但很適合用於學習如何自定義模版化控制項。

2.詳解

[ContentProperty(Name = "Content")]
[TemplatePart(Name = "PART_RootGrid", Type = typeof(Grid))]
[TemplatePart(Name = "PART_ExpanderToggleButton", Type = typeof(ToggleButton))]
[TemplatePart(Name = "PART_LayoutTransformer", Type = typeof(LayoutTransformControl))]
[TemplateVisualState(Name = "Expanded", GroupName = "ExpandedStates")]
[TemplateVisualState(Name = "Collapsed", GroupName = "ExpandedStates")]
[TemplateVisualState(Name = "LeftDirection", GroupName = "ExpandDirectionStates")]
[TemplateVisualState(Name = "DownDirection", GroupName = "ExpandDirectionStates")]
[TemplateVisualState(Name = "RightDirection", GroupName = "ExpandDirectionStates")]
[TemplateVisualState(Name = "UpDirection", GroupName = "ExpandDirectionStates")]
public class Expander : ContentControl
{
    public Expander();

    public string Header { get; set; }

    public DataTemplate HeaderTemplate { get; set; }

    public bool IsExpanded { get; set; }

    public ExpandDirection ExpandDirection { get; set; }

    public event EventHandler Expanded;

    public event EventHandler Collapsed;

    public void OnExpandDirectionChanged();
    protected override void OnApplyTemplate();
    protected virtual void OnCollapsed(EventArgs args);
    protected virtual void OnExpanded(EventArgs args);
}

以上是Expander的代碼定義,可以看出這個控制項十分簡單。本文首先對代碼和XAML做個詳細瞭解。這部分完全是面向初學者的,希望初學者通過Expander的源碼學會一個基本的模板化控制項應該如何構造。

2.1 Attribute

Expander定義了三種Attribute:ContentProperty、TemplatePart和TemplateVisualState。
ContentProperty表明瞭主要屬性為Content,並且在XAML中可以將Content屬性用作直接內容,即將這種代碼:

<controls:Expander>
    <controls:Expander.Content>
        <TextBlock Text="Text" />
    </controls:Expander.Content>
</controls:Expander>

簡化成如下形式:

<controls:Expander>
    <TextBlock Text="Text" />
</controls:Expander>

因為Expander本來就繼承自ContentControl,我很懷疑定義這個ContentProperty的必要性。(如果各位清楚這裡這麼做的原因請告知,謝謝。)

TemplatePart表明ControlTemplate中應該包含名為PART_ExpanderToggleButton的ToggleButton、名為PART_RootGrid的Grid及名為PART_LayoutTransformer的LayoutTransformControl。

TemplateVisualState表明ControlTempalte中應該包含名為ExpandedStates的VisualStateGroup,其中包含名為Expanded和Collapsed的兩種VisualState。另外還有名為ExpandDirectionStates的VisualStateGroup,其中包含RightDirection、LeftDirection、UpDirection和DownDirection。

即使ControlTemplate中沒按TemplatePart和TemplateVisualState的要求定義,Expander也不會報錯,只是會缺失部分功能。

2.2 Header與HeaderTemplate

PART_ExpanderToggleButton的Content和ContentTemplate通過TemplateBinding綁定到Expander的Header和HeaderTemplate,通過HeaderTemplate,Expander的Header外觀可以有一定的靈活性。

2.3 IsExpanded

Expander通過IsExpanded屬性控制內容是否展開。註意這是個依賴屬性,即這個屬性也可以通過Binding控制。在改變IsExpanded值的同時會依次調用VisualStateManager.GoToState(this, StateContentExpanded, true);OnExpanded(EventArgs args)ExpandedVisualStateManager.GoToState(this, StateContentCollapsed, true);OnCollapsedCollapsed
OnExpandedOnCollapsed都是protected virtual 函數,可以在派生類中修改行為。

許多人實現Expander時不使用IsExpanded屬性,而是通過public void Expand()public void Collapse()直接控制內容展開和摺疊,這種做法稍微缺乏靈活性。如PART_ExpanderToggleButton通過TwoWay Binding與IsExpanded屬性關聯,如果只提供public void Expand()public void Collapse()則做不到這個功能。

<ToggleButton x:Name="PART_ExpanderToggleButton" 
              IsChecked="{Binding IsExpanded, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" />

另一個常見的做法是通過代碼直接控制內容是否顯示,例如這樣:PART_MainContent.Visibility = Visibility.Collapsed;。這樣的壞處是不能在這個過程自定義動畫效果或進行其它操作。Expander通過VisualStateManager實現這個功能,做到了UI和代碼分離。

2.4 OnApplyTemplate

模板化控制項在載入ControlTemplate後會調用OnApplyTemplate(),Expander的OnApplyTemplate()實現了通常應有的實現,即訂閱事件、改變VisualState。

protected override void OnApplyTemplate()
{
    base.OnApplyTemplate();

    if (IsExpanded)
    {
        VisualStateManager.GoToState(this, StateContentExpanded, false);
    }
    else
    {
        VisualStateManager.GoToState(this, StateContentCollapsed, false);
    }

    var button = (ToggleButton)GetTemplateChild(ExpanderToggleButtonPart);

    if (button != null)
    {
        button.KeyDown -= ExpanderToggleButtonPart_KeyDown;
        button.KeyDown += ExpanderToggleButtonPart_KeyDown;
    }

    OnExpandDirectionChanged();
}

控制項在載入ControlTemplate時就需要確定它的狀態,一般這時候都不會使用過渡動畫。所以這裡VisualStateManager.GoToState(this, StateContentExpanded, false)的參數useTransitions使用了false。
由於Template可能多次載入(實際很少發生),或者不能正確獲取TemplatePart,所以使用TemplatePart前應該先判斷是否為空;如果要訂閱事件,應該先取消訂閱。

2.5 Style

<Style TargetType="controls:Expander">
    <Setter Property="Header" Value="Header" />
    <Setter Property="IsTabStop" Value="False" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="controls:Expander">
                <Grid>
                    <VisualStateManager.VisualStateGroups>
                        <VisualStateGroup x:Name="ExpandedStates">
                            <VisualState x:Name="Expanded">
                                <VisualState.Setters>
                                    <Setter Target="PART_MainContent.Visibility" Value="Visible" />
                                </VisualState.Setters>
                            </VisualState>
                            <VisualState x:Name="Collapsed">
                                <VisualState.Setters>
                                    <Setter Target="PART_RootGrid.Background" Value="Transparent" />
                                </VisualState.Setters>
                            </VisualState>
                        </VisualStateGroup>

                        <VisualStateGroup x:Name="ExpandDirectionStates">
                            ....
                        </VisualStateGroup>
                    </VisualStateManager.VisualStateGroups>

                    <Grid x:Name="PART_RootGrid" Background="{TemplateBinding Background}">
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                        </Grid.RowDefinitions>

                        <Grid.ColumnDefinitions>
                            <ColumnDefinition x:Name="ColumnOne" Width="Auto" />
                            <ColumnDefinition x:Name="ColumnTwo" Width="*" />
                        </Grid.ColumnDefinitions>

                        <controls:LayoutTransformControl Grid.Row="0" Grid.RowSpan="1" Grid.Column="0" Grid.ColumnSpan="2"
                                                         x:Name="PART_LayoutTransformer"
                                                         RenderTransformOrigin="0.5,0.5">
                            <controls:LayoutTransformControl.Transform>
                                <RotateTransform x:Name="RotateLayoutTransform" Angle="0" />
                            </controls:LayoutTransformControl.Transform>

                            <ToggleButton x:Name="PART_ExpanderToggleButton" 
                                          Height="40"
                                          TabIndex="0"
                                          AutomationProperties.Name="Expand"
                                          Style="{StaticResource HeaderToggleButtonStyle}" 
                                          VerticalAlignment="Bottom" HorizontalAlignment="Stretch" 
                                          Foreground="{TemplateBinding Foreground}"
                                          Background="{TemplateBinding Background}"
                                          ContentTemplate="{TemplateBinding HeaderTemplate}" Content="{TemplateBinding Header}"
                                          IsChecked="{Binding IsExpanded, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" />
                        </controls:LayoutTransformControl>

                        <ContentPresenter Grid.Row="1" Grid.RowSpan="1" Grid.Column="0" Grid.ColumnSpan="2"
                                          x:Name="PART_MainContent"
                                          Background="{TemplateBinding Background}"
                                          HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                                          HorizontalContentAlignment="Stretch"
                                          VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
                                          Visibility="Collapsed" />
                    </Grid>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

如果忽略ExpandDirectionStates,Expander的Style就如以上所示十分簡短(不過HeaderToggleButtonStyle有整整300行)。註意 Setter Property="IsTabStop" Value="False" 這句,對內容控制項或複合控制項,約定俗成都需要將IsTabStop設置成False,這是為了防止控制項本身獲得焦點。對Expander來說,在前一個控制項上按“Tab”鍵,應該首先讓PART_ExpanderToggleButton獲得焦點。如果IsTabStop="true",Expander會獲得焦點,需要再按一次“Tab”鍵才能讓PART_ExpanderToggleButton獲得焦點。

2.6 partial class

即使代碼量不大,Expander還是將代碼分別存放在幾個partial class中,這樣做的好處是讓承載主要業務的文件(Expander.cs)結構更加清晰。尤其是依賴屬性,一個完整的依賴屬性定義可以有20行(屬性標識符、屬性包裝器、PropertyChangedCallback等),而且其中一部分是靜態的,另外一部分不是,在類中將一個依賴屬性的所有部分放在一起,還是按靜態、非靜態的順序存放,這也可能引起爭論。

2.7 其它

雖然Expander是一個教科書式的控制項,但還是有幾個可以改進的地方。

最讓人困擾的一點是Header居然是個String。WPF中的Expander的Header是個Object,可以方便地塞進各種東西,例如一個CheckBox或一張圖片。雖然通過更改ControlTemplate或HeaderTemplate也不是不可以達到這效果,但畢竟麻煩了一些。不久前MenuItem就把Header從String類型改為Object了(Menu: changed MenuItem Header to type object),說不定以後Expander也有可能這樣修改( Change Expander.Header from string to object )。

另外,在WPF中Expander派生自HeaderedContentControl,這就少寫了Header、HeaderTemplate、OnHeaderChanged等一大堆代碼。而Community Toolkit中每個有Header屬性的控制項都各自重覆了這些代碼。或許將來會有HeaderedContentControl這個控制項吧。

PART_ExpanderToggleButton滑鼠按下時Header和Content分裂的效果還挺奇怪的,這點在上一篇文章有提過( 淺談按鈕設計)。

最後,這年頭連個摺疊/展開動畫都沒有,而且還是微軟出品,真是可惜(Improve Expander control (animation, color))。還好XAML擴展性確實優秀,可以自己添加這些動畫。

3. 擴展

我簡單地用Behavior為Expander添加了摺疊/展開動畫,代碼如下:

public class PercentageToHeightBehavior : Behavior<StackPanel>
{
    /// <summary>
    /// 獲取或設置ContentElement的值
    /// </summary>  
    public FrameworkElement ContentElement
    {
        get { return (FrameworkElement)GetValue(ContentElementProperty); }
        set { SetValue(ContentElementProperty, value); }
    }

      
    protected virtual void OnContentElementChanged(FrameworkElement oldValue, FrameworkElement newValue)
    {
        if (oldValue != null)
            newValue.SizeChanged -= OnContentElementSizeChanged;

        if (newValue != null)
            newValue.SizeChanged += OnContentElementSizeChanged;
    }

    private void OnContentElementSizeChanged(object sender, SizeChangedEventArgs e)
    {
        UpdateTargetHeight();
    }


    /// <summary>
    /// 獲取或設置Percentage的值
    /// </summary>  
    public double Percentage
    {
        get { return (double)GetValue(PercentageProperty); }
        set { SetValue(PercentageProperty, value); }
    }

      
    protected virtual void OnPercentageChanged(double oldValue, double newValue)
    {
        UpdateTargetHeight();
    }


    public event PropertyChangedEventHandler PropertyChanged;

    private void UpdateTargetHeight()
    {
        double height = 0;
        if (ContentElement == null || ContentElement.ActualHeight == 0 || double.IsNaN(Percentage))
            height = double.NaN;
        else
            height = ContentElement.ActualHeight * Percentage;

        if (AssociatedObject != null)
            AssociatedObject.Height = height;
    }
}
<Grid>
    <VisualStateManager.VisualStateGroups>
        <VisualStateGroup x:Name="ExpandedStates">
            <VisualStateGroup.Transitions>
                <VisualTransition  To="Collapsed">
                    <Storyboard>
                        <DoubleAnimation Duration="0:0:0.5"
                                         To="0"
                                         Storyboard.TargetProperty="(UIElement.Opacity)"
                                         Storyboard.TargetName="PART_MainContent">
                            <DoubleAnimation.EasingFunction>
                                <CubicEase EasingMode="EaseOut" />
                            </DoubleAnimation.EasingFunction>
                        </DoubleAnimation>
                        <DoubleAnimation BeginTime="0:0:0"
                                         Duration="0:0:0.5"
                                         To="0"
                                         Storyboard.TargetProperty="(local:PercentageToHeightBehavior.Percentage)"
                                         Storyboard.TargetName="PercentageToHeightBehavior"
                                         EnableDependentAnimation="True">
                            <DoubleAnimation.EasingFunction>
                                <QuinticEase EasingMode="EaseIn" />
                            </DoubleAnimation.EasingFunction>
                        </DoubleAnimation>
                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)"
                                                       Storyboard.TargetName="PART_MainContent">
                            <DiscreteObjectKeyFrame KeyTime="0:0:0.5">
                                <DiscreteObjectKeyFrame.Value>
                                    <Visibility>Collapsed</Visibility>
                                </DiscreteObjectKeyFrame.Value>
                            </DiscreteObjectKeyFrame>
                        </ObjectAnimationUsingKeyFrames>
                    </Storyboard>
                </VisualTransition>
                <VisualTransition GeneratedDuration="0"
                                  To="Expanded">
                    <Storyboard>
                        <DoubleAnimation BeginTime="0:0:0.0"
                                         Duration="0:0:0.5"
                                         To="1"
                                         Storyboard.TargetProperty="(UIElement.Opacity)"
                                         Storyboard.TargetName="PART_MainContent">
                            <DoubleAnimation.EasingFunction>
                                <CubicEase EasingMode="EaseIn" />
                            </DoubleAnimation.EasingFunction>
                        </DoubleAnimation>
                        <DoubleAnimation BeginTime="0:0:0"
                                         Duration="0:0:0.5"
                                         To="1"
                                         Storyboard.TargetProperty="(local:PercentageToHeightBehavior.Percentage)"
                                         Storyboard.TargetName="PercentageToHeightBehavior"
                                         EnableDependentAnimation="True">
                            <DoubleAnimation.EasingFunction>
                                <QuinticEase EasingMode="EaseOut" />
                            </DoubleAnimation.EasingFunction>
                        </DoubleAnimation>
                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)"
                                                       Storyboard.TargetName="PART_MainContent">
                            <DiscreteObjectKeyFrame KeyTime="0:0:0">
                                <DiscreteObjectKeyFrame.Value>
                                    <Visibility>Visible</Visibility>
                                </DiscreteObjectKeyFrame.Value>
                            </DiscreteObjectKeyFrame>
                        </ObjectAnimationUsingKeyFrames>
                    </Storyboard>
                </VisualTransition>
            </VisualStateGroup.Transitions>
            <VisualState x:Name="Expanded">
                <VisualState.Setters>
                    <Setter Target="PART_MainContent.(UIElement.Opacity)"
                            Value="1" />
                    <Setter Target="PercentageToHeightBehavior.(local:PercentageToHeightBehavior.Percentage)"
                            Value="1" />
                    <Setter Target="PART_MainContent.Visibility"
                            Value="Visible" />
                </VisualState.Setters>
            </VisualState>
            <VisualState x:Name="Collapsed">
            </VisualState>
        </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <ToggleButton x:Name="PART_ExpanderToggleButton"
                      Height="40"
                      TabIndex="0"
                      AutomationProperties.Name="Expand"
                      Style="{StaticResource HeaderToggleButtonStyle}"
                      VerticalAlignment="Top"
                      HorizontalAlignment="Stretch"
                      Foreground="{TemplateBinding Foreground}"
                      Background="{TemplateBinding Background}"
                      ContentTemplate="{TemplateBinding HeaderTemplate}"
                      Content="{TemplateBinding Header}"
                      IsChecked="{Binding IsExpanded, Mode=TwoWay, RelativeSource={RelativeSource Mode=TemplatedParent}}" />
        <StackPanel x:Name="stackPanel"
                    Grid.Row="1">
            <Interactivity:Interaction.Behaviors>
                <local:PercentageToHeightBehavior x:Name="PercentageToHeightBehavior"
                                                  ContentElement="{Binding ElementName=PART_MainContent}"
                                                  Percentage="0" />
            </Interactivity:Interaction.Behaviors>
            <ContentPresenter x:Name="PART_MainContent"
                              Background="{TemplateBinding Background}"
                              HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                              HorizontalContentAlignment="Stretch"
                              VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
                              Visibility="Collapsed"
                              Opacity="0" />
        </StackPanel>
    </Grid>
</Grid>

原理是把ContentPresenter放進一個StackPanel里,通過DoubleAnimation改變這個StackPanel的高度。之所以不直接改變ContentPresenter的高度是不想改變它的內容高度。另外我也改變了PART_ExpanderToggleButton的動畫效果,我有點討厭滑鼠按下時文字會變模糊這點。運行效果如下:

4. 結語

寫這篇文章拖了很多時間,正好2.0版本也發佈了( Releases · Microsoft_UWPCommunityToolkit ),所以截圖及源碼有一些是不同版本的,但不影響主要內容。
如前言所說,這真的是個很好的入門級控制項,很適合用於學習模板化控制項。

5. 參考

Expander Control
Microsoft.Toolkit.Uwp.UI.Controls.Expander

6. 源碼

GitHub - ExpanderDemo
因為是在v1.5.0上寫的,可能需要修改才能使用到v2.0.0上。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 前言:轉載 陳浩一個從事安全運維向的前輩文章。寫的很好。人非常nice,遇到了問題,qq上很快就回覆了我。 大道三千 入門最難,凡事入了行,也就什麼都好說了,好的自然不斷努力奮鬥修行,不好的自然很快就被淘汰。恭謹勤勉,時不我待~ it自動化運維就是要很方便的運用各種工具進行管理維護,有效的實施伺服器 ...
  • 1.環境: ubuntu16.04 2.背景: 想更換下位機內核 3.使用kermit進行串口傳輸 舉例:傳輸文件到下位機 2.1首先進入下位機的uboot 2.2 使用uboot自帶的命令從串口接收數據 loadb 將接收到的數據放到記憶體的某個位置 比如: loadb 0x42000000 2.3 ...
  • 1.環境: Ubuntu 16.04 2.獲取 wget mirrors.ustc.edu.cn/gnu/gcc/gcc-4.8.3/gcc-4.8.3.tar.bz2 3.解壓 tar xvf gcc-4.8.3.tar.bz2 4.切換目錄 cd gcc-4.8.3 5.下載mpfr等依賴庫 . ...
  • (1)普通文件(regular file):這是最常用的文件類型,這種文件包含了某種形式的數據,文件內容的解釋由處理該文件的應用程式進行。 (2)目錄文件(directory file):這種文件包含了其他文件的 名字以及指向這些文件有關信息的指針。對一個目錄文件具有讀許可權的進程,都可以讀該目錄的內 ...
  • 第二天,繼續學習Linux命令。。。 一、查看文件和目錄列表的命令 ls:顯示當前目錄下的文件和目錄,但是不會顯示隱藏的文件和目錄。 ls -a:顯示當前目錄下的所有文件和目錄。 ls -l:顯示當前目錄下的文件和目錄的一些詳細信息,其中包括: 文件類型:比如目錄(d)、文件(-)、字元型文件(c) ...
  • 關鍵字:StructLayout、LayoutKind.Explicit、FieldOffset 輸出的值: 欄位定義交換順序: 同樣的偏移量,輸出的值以最後一個欄位的值為準。 CLR保持開發人員的定義欄位的順序。 ...
  • 系統環境 操作系統:CentOS6.9關閉防火牆 安裝步驟 1. 安裝Postfix 2. 下載rpm包並安裝 3. 配置gitlab,vim /etc/gitlab/gitlab.rb,指定ip+埠號 4. 執行配置並啟動。--gitlab-ctl reconfigure--gitlab-ctl ...
  • 1.新建項目選擇ASP.net MVC 4 Web應用程式 2.選擇Web API c 3.在新建立的項目裡面有已經生成的webapi模版 其中App_Start文件夾下WebApiConfig.cs和RouteConfig.cs文件主要配置api的路由信息。 Controllers文件夾下的文件為 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...