zoukankan      html  css  js  c++  java
  • WPF DependencyProperty

     

    o       Section1 :Brief introduction

    1. CLR properties are really just safe wrappers around a Private member variable:

    privateint x;
    publicint X
    {
        get { return x; }
        set { x = value; }
    }

    DependencyProperty(DP) is more than just simple CLR properties, The following table illustrates some of the things that can be acheived by the use of DPs:

    Achievable items thanks to DPs

    Change Notification

    Callbacks

    Property value validation

    Property value inheritence *

    Participation in animations *

    Participation in Styles *

    Participation in Templates *

    Databinding

    Layout changes *

    Overriding default data values *

     

     

    2. Declare of DependencyProperty:

    • Declare a dependencyProperty (Always public static readonly)
    • Initialise the dependencyProperty, either using DependencyProperty.RegisterAttached/DependencyProperty.Register/DependencyProperty.RegisterReadOnly/DependencyPropertyRegisterAttachedReadOnly
    • Declare get/set property wrapper (see code below)

     

    public class MyStackPanel : StackPanel
        {
            
            public static readonly DependencyProperty MinDateProperty;
            static MyStackPanel()
            {
                MinDateProperty = DependencyProperty.Register("MinDate",
                typeof(DateTime),
                typeof(MyStackPanel),
                new FrameworkPropertyMetadata(DateTime.MinValue, 
                FrameworkPropertyMetadataOptions.Inherits));
            }
            public DateTime MinDate
            {
                get { return (DateTime)GetValue(MinDateProperty); }
                set { SetValue(MinDateProperty, value); }
            }
     
        }

     

    Below is the register method:

    public static DependencyProperty Register(string name, Type propertyType, Type ownerType, PropertyMetadata typeMetadata, System.Windows.ValidateValueCallback validateValueCallback)

     

    The last parameter is the delegate of ValidateValue method. For PropertyMetadata parameter, we use FrameWorkPropertyMetadata , is the type used for dependency property metadata, rather than the base metadata types PropertyMetadata or UIPropertyMetadata. This is true both for existing dependency properties and for most custom dependency property scenarios.

    public FrameworkPropertyMetadata(object defaultValue, FrameworkPropertyMetadataOptions flags, PropertyChangedCallback propertyChangedCallback, CoerceValueCallback coerceValueCallback) : base(defaultValue, propertyChangedCallback, coerceValueCallback, bool isAnimationProhibited, UpdateSourceTrigger defaultUpdateSourceTrigger)
    • Default values
    • Provide one of the FrameworkPropertyMetadataOptions values, such as AffectsMeasure/AffectsArrange/AffectsRender/Inherits etc
    • Property changed callback delegates
    • Coersion values callback delegates ,can operate value
    • Make a property un-animatable
    • Provide one of the UpdateSourceTrigger, such as PropertyChanged/LostFocus/Explicit etc

    o       Section2 :How the Dependency Property registered and use

     

    DependencyProperty Class has a static hashtable and list to store the registered dependencyProperty,

    Class DependencyProperty
    {………………
    private static Hashtable PropertyFromName = new Hashtable();
    internal static ItemStructList<DependencyProperty> RegisteredPropertyList = new ItemStructList<DependencyProperty>(0x300);
    …………………..}

     

    Method Register 
    {………
    DependencyProperty dp = new DependencyProperty(name, propertyType, ownerType, defaultMetadata, validateValueCallback);
    ………………..
    PropertyFromName[key] = dp;
    RegisteredPropertyList.Add(dp);
    …………
    if (typeMetadata != null)
          {
             this.OverrideMetadata(ownerType, typeMetadata);
           }
     
    ……….}

     

    The key in PropertyFromName is “name.HashCode^ownerType.HashCode”.

    The Value of this dictionary is the instance of DependencyPropery.

     

    Then if client have new FrameWorkMetaData, registration will add the metadata into a instance map, which will be explained specifically below in AddOwner method.

     

    Take notice that the PropertyFromName hashtable and RegisteredPropertyList are static ,so all DPs can access these global hashtable and list, and let's see what members are instance members in DependencyProperty class:

     

     Class DependencyProperty
    {
             private PropertyMetadata _defaultMetadata;
            internal InsertionSortMap _metadataMap; 
            private string _name;
            private Type _ownerType;
            private Flags _packedData;
            private Type _propertyType;
            private DependencyPropertyKey _readOnlyKey;
            private System.Windows.ValidateValueCallback _validateValueCallback; 
    …………………
     private
    DependencyProperty(string name, Type propertyType, Type ownerType, PropertyMetadata defaultMetadata, System.Windows.ValidateValueCallback validateValueCallback)
          
      {           
        Flags uniqueGlobalIndex;           
       this._metadataMap = new InsertionSortMap();           
       this._name = name;           
       this._propertyType = propertyType;           
       this._ownerType = ownerType;           
       this._defaultMetadata = defaultMetadata;           
       this._validateValueCallback = validateValueCallback;
    }
    …………………….
    }

     

    Most of the instance members are initiated in DP’s private constructor. When register, the filed “_defaultMetadata used to store FrameWorkMetaData, then what _metadataMap used for?It used to store other controls metaData.

     

    Now, we have registered MinDateProperty, then if other control need to use this property ,it will use AddOwner method like below:

     

      public DependencyProperty AddOwner(Type ownerType, PropertyMetadata typeMetadata)
    {
    if (typeMetadata != null)
                {
                    this.OverrideMetadata(ownerType, typeMetadata);
                }
    lock (Synchronized)
                {
                    PropertyFromName[key] = this;
                }
    }

     

    If register a new same name property, it will have nothing to do with MyStackPanel.MinDateProperty even if use inherited flag and is children of MyStackPanel

     

    public partial class UserControlLabel : Label
     {
    public static readonly DependencyProperty MinDateProperty = MyStackPanel.MinDateProperty.AddOwner(typeof(UserControlLabel),
    new FrameworkPropertyMetadata(DateTime.MinValue,FrameworkPropertyMetadataOptions.Inherits));
    }

     

     

    AddOwner method will call OverrideMetadata method to Supplies alternate metadata for this dependency property when it is present on instances of a specified type, overriding the metadata that was provided in the initial property registration(when registeration ,if metadata is not null, also will call this method to put metadata in map):

             this._metadataMap[dType.Id] = typeMetadata;

    We can see this map, its key is current Dependency Object  type’s Id ,and value is metadata. Take our program as example:

    Instance member :_metadataMap in MyStackPanel.MindateProperty:

    UserControlLabel.typeId

    UserControlLabel ‘s MinDateProperty. metaData

    MyStackPanel.typeId

    MyStackPannel’s MinDateProperty metaData

     

     

     

     

    ( Note: if in AddOwner or register method, propertyMetaData == null, for register method, it will create a default propertyMetadata instance, for AddOwner method,it will don’t call OverrideMetadata method,_metaMap will no be changed, so how UserControlLabel find meta data for MinDatePropertymetaData?

    In MinDateProperty.GetMetaData() method , it first find meta data through UserControlLabel’s typeId,if can’t find, try UserControlLabel’s base type, if can’t either, return the MinDateProperty’s default property metaData.)所以在写onPropertyChanged方法时候,要判断 source as StackPanel != null, 因为一个metadata 可能被其他source用到,这时候source就不是stackPanel了。   

    then add <propertyname ^ ownerType, property instance> to Hashtable PropertyFromName ,now the hashtable contains two elments, key is different ,but value is same: 

    Static member:PropertyFromName hashtable in MinDateProperty:

    MinDate^StackPanel

    StackPanel’s MinDateProperty instance member

    MinDate^UserControl

    StackPanel’s MinDateProperty instance member

     

     

     

     

               

     

    This PropertyFromName hashtable is mainly use by the xaml->code process which can be found by analyzing the DependencyProperty.FromName() method using reflectors use by function..

     

    namespace System.Windows.Markup

    internal class BamlMapTable

    {

    internal DependencyProperty GetDependencyProperty(BamlAttributeInfoRecord bamlAttributeInfoRecord)

            {

                if ((bamlAttributeInfoRecord.DP == null) && (bamlAttributeInfoRecord.PropInfo == null))

                {

                    this.GetAttributeOwnerType(bamlAttributeInfoRecord);

                    if (bamlAttributeInfoRecord.OwnerType != null)

                    {

                        bamlAttributeInfoRecord.DP = DependencyProperty.FromName(bamlAttributeInfoRecord.Name, bamlAttributeInfoRecord.OwnerType);

                    }

                }

                return bamlAttributeInfoRecord.DP;

            }

    }

    SomeTimes if we add other classs DP as a member in my class, we can directly use overrideMetadata method to override metadata:

    public void OverrideMetadata(Type forType, PropertyMetadata typeMetadata)

     

    Class Page:Control
    Static Page
    {  
    UIElement.FocusableProperty.OverrideMetadata(typeof(Page), new FrameworkPropertyMetadata(BooleanBoxes.FalseBox));
    }
    }

    The difference between use AddOwner method and overrideMetadata method is the last one don’t need to add <propertyname ^ ownerType, property instance> to Hashtable PropertyFromName.

      

    Use OverrideMetaData method 意味着这个control并不想把这个Property作为自己的property,xaml里可以直接设置了,也就是不必写.net property的包装,因为这个根本不是它的属性,一般用于attached property或者基类的property override  metadata.

    In DependencyProperty Class, also have method:

       public static DependencyPropertyKey RegisterReadOnly(string name, Type propertyType, Type ownerType, PropertyMetadata typeMetadata, System.Windows.ValidateValueCallback validateValueCallback)

    This method is used to register a read-only dependency property ,it also will new a DP instance ,fill hashtable and metamap, at last return DependencyPropertyKey class:

    public sealed class DependencyPropertyKey
    {
        // Fields
        private DependencyProperty _dp;
     
        // Methods
        internal DependencyPropertyKey (DependencyProperty dp);
        public void OverrideMetadata(Type forType, PropertyMeta typeMetadata);
        internal void SetDependencyProperty (DependencyProperty dp);
     
        // Properties
        public DependencyProperty DependencyProperty{ get; }

    }

     

    o       Section3 :How to get and Set Property Value

    All WPF class which use dependency property should inherited DependencyObject Class, in DependencyObject , it has methods SetValue(DependencyProperty dp, bool value) to set Value for one dependency Property and  public object GetValue(DependencyProperty dp)to get Value for one dependency Property. (In program, code will use property wrapper to set and get value, but in xaml, will directly use SetValue() and GetValue() method).

     

     In DependencyObject Class, it uses array

    private EffectiveValueEntry[] _effectiveValues 

    to store the dependencyProperty’s value. The EffectiveValueEntry is not a simple value structure, it includes much information about the pipeline to get value, you can see appendix picture specifically.

     

    In SetValue(DependencyProperty dp, bool value), it firstly get property metadata for this control from _metadataMap which I have said in front section. How to get it? Haven’t you see the map? Through the XXXClass.typeId ( PropertyMetadata metadata = this.SetupPropertyChange(dp))

     

    SomeTimes, we don’t know the instance of Dependency property, we can also get it from PropertyFromName Hashtable, use method below which is in DependencyProperty class.

     

    [FriendAccessAllowed]
           internal static DependencyProperty FromName(string name, Type ownerType).

     

    Now,Let’s save the value into _effectiveValues array. But before it, there are complicated things to do, because WPF contains many powerful mechanisms that independently attempt to set the value of dependency properties. Of course, as their name indicates, dependency properties were designed to depend on these providers in a consistent and orderly manner.

                The picture below illustrates the five-step process(we call it pipeline) that WPF runs each dependency property through in order to calculate its final value. This process happens automatically thanks to the built-in change notification in dependency properties.

     

     

     

                In SetValue method, it firstly will get corresponding dependencyProperty EffectiveValueEntry(if already have, get it, if not already have, new it). _effectiveValues is an array, how do I know which element is my want? _effectiveValues array use DependencyProperty’s hashcode as index, this hashcode is called DependencyProperty.GlobalIndex , it is generated in dependecyProperty ‘s constructor:


    lock (Synchronized)
                {
                  uniqueGlobalIndex = (Flags) GetUniqueGlobalIndex(ownerType, name);
                    RegisteredPropertyList.Add(this);
                }
                if (propertyType.IsValueType)
                {
                    uniqueGlobalIndex |= Flags.IsValueType;
                }
                if (propertyType == typeof(object))
                {
                    uniqueGlobalIndex |= Flags.IsObjectType;
                }
                if (typeof(Freezable).IsAssignableFrom(propertyType))
                {
                    uniqueGlobalIndex |= Flags.IsFreezableType;
                }
                if (propertyType == typeof(string))
                {
                    uniqueGlobalIndex |= Flags.IsStringType;
                }
                this._packedData = uniqueGlobalIndex;
                 GlobalIndex =(int) this._packedData) & 0xffff;
              }

                Next go to the pipeline,
    Step1:“determine Base Value”:
    The following list reveals the eight providers that can set the value of most dependency properties, in order from highest to lowest precedence:

    1. Local value

    2. Style triggers

    3. Template triggers

    4. Style setters

    5. Theme style triggers

    6. Theme style setters

    7. Property value inheritance

    8. Default value

                Here I take inheritance for example; you will see how the inheritance is implemented. The code is:

     

    if (!flag4 && metadata.IsInherited)
           {
             DependencyObject inheritanceParent = this.InheritanceParent;
              if (inheritanceParent != null)
              {
                 EntryIndex entry = inheritanceParent.LookupEntry(dp.GlobalIndex);
                 if (entry.Found)
                 {
                  flag4 = true;
    newEntry=inheritanceParent._effectiveValues[entry.Index].GetFlattenedEntry(RequestFlags.FullyResolved);
                  newEntry.BaseValueSourceInternal = BaseValueSourceInternal.Inherited;
                     }
                 }
            }

    Step2:Expression: If the value from step one is an expression (an object deriving from System.Windows.Expression), then WPF performs a special evaluation step to convert the expression into a concrete result.

    Step3: Animation: If one or more animations are running, they have the power to alter the current property value (using the value after step 2 as input) or completely replace it.

    Step4: if in metadata, have set coerceValueCallback delegate ,will call this delegate to operatet the value.The code like:

     

    if ((metadata.CoerceValueCallback != null)
    {
    object obj6 = metadata.CoerceValueCallback(this, coersionBaseValue);
    }

     

    Step5: When register property, we set ValidateValueCallback delegate, will call delegate to validate this property value.

     

    (Most of the above 5 process is in method below, even can see it as an event trigger method:

     

    Internal UpdateResult UpdateEffectiveValue(EntryIndex entryIndex, DependencyProperty dp, PropertyMetadata metadata, EffectiveValueEntry oldEntry, ref EffectiveValueEntry newEntry, bool coerceWithDeferredReference, OperationType operationType))

    )

    After the five steps, save the EffectiveValueEntry  which has the value to _effectiveValues[DependencyProperty.GlobalIndex] .

     

    Don’t forget we can add propertyChangedCallback delegate to metadata when regester the DP, at last in setValue() , if value changed, it will call this delegate :

     

     

     

    if ((isAValueChange)
               {               
     this.NotifyPropertyChange(new DependencyPropertyChangedEventArgs(dp, metadata, isAValueChange, oldEntry, newEntry, operationType));
                }
     
     
    internal void NotifyPropertyChange(DependencyPropertyChangedEventArgs args)
            {
                this.OnPropertyChanged(args);
                ………………………
    }
     
     
    Protected virtual void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
    {
    if ((e.IsAValueChange || e.IsASubPropertyChange) || (e.OperationType == OperationType.ChangeMutableDefaultValue))
                {
                    PropertyMetadata metadata = e.Metadata;
                    if ((metadata != null) && (metadata.PropertyChangedCallback != null))
                    {
                        metadata.PropertyChangedCallback(this, e);
                    }
                }
    }

     

    Use GetValue() method to get DepencyProperty value  from _effectiveValues array using DependencyProperty.GlobalIndex, if have no value, GetValue method will try to get value from default, or if have set inherited flag in metaData,it will try to get value from parent .

     

    In DependencyObject ,there is one useful method ClearValue(DependencyProperty), it can clear the local value(because it has highest precedence), and call UpdateEffectiveValue method to run the pipeline to get value again,  and this method can call PropertyChanged delegate too.

    o       Section4 :Attached Dependency Property

    Attached properties are just another strain of DPs. Using Attached Properties we are able to use DPs from classes that are outside of the current class.

    1. Register Attached DP: use RegisterAttached method

    public class MyStackPanel : StackPanel
        {
               public static readonly DependencyProperty IntDataProperty = DependencyProperty.RegisterAttached("IntData",
              typeof(int),
              typeof(MyStackPanel),
              new FrameworkPropertyMetadata(0,
              FrameworkPropertyMetadataOptions.Inherits, onIntdataChange, onIntdataCoreceValue),onIntdataValidate);

    }

      

    The ownerType can be not DependencyObject, so RegisterAttached will not call overrideMetaData. Because _metadataMap use DependencyObjectType.typeId as key.

    2. Provide static GetPropertyName and SetPropertyName methods as accessors for the attached property.

     

     

     public class MyStackPanel : StackPanel
    {     
             public static void SetIntData(UIElement element, int value)
            {
                 //actually
    DependencyObject.SetValue()
                element.SetValue(IntDataProperty, value); 
            }
            public static int GetIntData(UIElement element)
            {
                 //actually DependencyObject.GetValue()
                return (int)element.GetValue(IntDataProperty);
            }
    }

     

    3. Use attached property.

     

          <Label x:Name="myLabel" FontWeight="Bold" FontSize="20" Foreground="White" Panel.ZIndex="5" ab:MyStackPanel.IntData="4">
     
    The corresponding coding about attached property is :
     
         MyStackPanel.SetIntData(myLabel,4);

     

    4. Effect.  If in MyStackPanel , I have set onPropertyChange delegate ,in the method ,I can operate the label control which have changed the property. In this code below , the mylabel ‘s background will be set to pink.

     

    private static void onIntdataChange(DependencyObject o, DependencyPropertyChangedEventArgs e)
            {
                if (o is Control)
                {
                    Control sourceControl = o as Control;
                    sourceControl.Background = Brushes.Pink;
                }
            }

      

    Attached Property’s function:

    1.      Can add functions for other class and not changing their code. Like the upper step 4

    2.      Save data to _EffectiveValue[] , get it when need to use it:

    <Button Canvas.Left=18 Canvas.Top=18Background=Orange>Left=18,

     

         What’s the difference between DependencyProperty.RegisterAttached() andDependencyProperty.Register() ? Just at the metadata, RegisterAttached method will not call OverrideMetadata method, this method will do two things which have stated in section2 , one is combine the base type’s metadata with new metadata ,the other is add metadata to instance member _metadataMap.Why the attache property dont do these two things ,I have not understood it.  

    Although it, RegisterAttached method still will “new” a DependencyProperty instance ,add it into static member: RegisteredPropertyList and PropertyFromName hash table like Register method do

    .

          We must know  this “IntDate” Dependency property  and its value are belong to “MyLabel” instance , not belong to “MyStackPanel” instance any more.  Then we get to know why we should add static GetPropertyName and SetPropertyName methods. It need use “MyLabel”’s SetValue() method to store the “IntDate” DP and its value into “MyLabel”’s  _effectiveValues array.

         Then “MyLabel” will get metadata from IntDataProperty , and trigger the PropertyChangeCallBack and coerceValueCallback , because pass the “MyLabel” instance to these call back, so can operate “MyLabel” instance.

     

       

    MyLabel’s _effectiveValues[]

     

     

    Index = IntDataProperty.GlobalIndex

    Value=4

        

                                

     


    Appendix 1:

     

     

    Appendix 2:

     

     

     

    Appendix 3:

    In register and AddOwner method , they all will  use the OverrideMetadata method:

     

    public static DependencyProperty Register(string name, Type propertyType, Type ownerType, PropertyMetadata typeMetadata, System.Windows.ValidateValueCallback validateValueCallback)
    {
                PropertyMetadata defaultMetadata = null;
                if ((typeMetadata != null) && typeMetadata.DefaultValueWasSet())
                {
                    defaultMetadata = new PropertyMetadata(typeMetadata.DefaultValue);
                }
                DependencyProperty property = RegisterCommon(name, propertyType, ownerType, defaultMetadata, validateValueCallback);
                if (typeMetadata != null)
                {
                    property.OverrideMetadata(ownerType, typeMetadata);
                }
                return property;
    }

     

    OverrideMetaData method:

     

    public void OverrideMetadata(Type forType, PropertyMetadata typeMetadata)
            {
                DependencyObjectType type;
                PropertyMetadata metadata;
                this.SetupOverrideMetadata(forType, typeMetadata, out type, out metadata);
     
                this.ProcessOverrideMetadata(forType, typeMetadata, type, metadata);
            }

     

     

    I said in section 2, its main job is adding new metadata to _metadataMap structure. But already have confusing issue which I don’t understand.

     

    private void SetupOverrideMetadata(Type forType, PropertyMetadata typeMetadata, out DependencyObjectType dType, out PropertyMetadata baseMetadata)
    {
         // get a wrapper struct DependencyObjectType which wrape the owner type
        dType = DependencyObjectType.FromSystemType(forType);
     
        //get owner type’s directly base type’s metadata
        baseMetadata = this.GetMetadata(dType.BaseType);
    }

     

     

    private void ProcessOverrideMetadata(Type forType, PropertyMetadata typeMetadata, DependencyObjectType dType, PropertyMetadata baseMetadata)
            {
                lock (Synchronized)
                {
                     // set map I have said in section2
                    this._metadataMap[dType.Id] = typeMetadata;
                }
                typeMetadata.InvokeMerge(baseMetadata, this);
                typeMetadata.Seal(this, forType);
             }
     

     The confusing issue is “typeMetadata.InvokeMerge” method, it seems combine the base type’s metadata with current metadata:

     

    protected virtual void Merge(PropertyMetadata baseMetadata, DependencyProperty dp)
    {
             if (baseMetadata.PropertyChangedCallback != null)
                {
                    Delegate[] invocationList = baseMetadata.PropertyChangedCallback.GetInvocationList();
                    if (invocationList.Length > 0)
                    {
                        System.Windows.PropertyChangedCallback a = (System.Windows.PropertyChangedCallback) invocationList[0];
                        for (int i = 1; i < invocationList.Length; i++)
                        {
                            a = (System.Windows.PropertyChangedCallback) Delegate.Combine(a, (System.Windows.PropertyChangedCallback) invocationList[i]);
                        }
                        a = (System.Windows.PropertyChangedCallback) Delegate.Combine(a, this._propertyChangedCallback);
     
                        this._propertyChangedCallback = a;
                    }
                }
                if (this._coerceValueCallback == null)
                {
                    this._coerceValueCallback = baseMetadata.CoerceValueCallback;
                }
    }

    In code ,it seems to combine this DP’s propertyChangeCallback with baseMetaData’s propertyChangeCallback, and if this._coerceValueCallback== null ,use baseMetadata.CoerceValueCallback.

     

    But I have made an experiment:

     

    public class MyStackPanel : StackPanel

        {

           

            public static readonly DependencyProperty MinDateProperty;

            static MyStackPanel()

            {

                MinDateProperty = DependencyProperty.Register("MinDate",

                typeof(DateTime),

                typeof(MyStackPanel),

                new FrameworkPropertyMetadata(DateTime.MinValue,             FrameworkPropertyMetadataOptions.Inherits,onMindateChange,onMindateCoreceValue),onMindateValidate);

    }

      public DateTime MinDate

            {

                get { return (DateTime)GetValue(MinDateProperty); }

                set { SetValue(MinDateProperty, value); }

            }

    …………………

    }

      public class MyStackPanel2 : MyStackPanel

        {

       public static readonly  DependencyProperty MinDateProperty =DependencyProperty.Register("MinDate",

                typeof(DateTime),

                typeof(MyStackPanel2), new FrameworkPropertyMetadata(DateTime.MinValue ,

              FrameworkPropertyMetadataOptions.Inherits,onMindateChange2), onIntdataValidate);

     

            public new DateTime  MinDate

            {

                get { return (DateTime)GetValue(MinDateProperty); }

                set { SetValue(MinDateProperty, value); }

            }

            private static void onMindateChange2(DependencyObject o, DependencyPropertyChangedEventArgs e)

            {       

            }

        }

     

    It’s a pity,when set value for MyStackPanel2.Mindate,it will trigger onMindateChange2 method, its parent MyStackPanel.onMindateChange will not be triggered , so do the onMindateCoreceValue.

     

    So ,I don’t know the merge in overrideMetadata is used for what?

  • 相关阅读:
    问题建模---大纲---待补充
    塞库报表封装问题分析--一篇不太成功的问题分析报告
    哲学的根本问题--以人为本
    什么是本体论
    知行合一是做人的最高境界
    什么是问题?--人类才是最大的问题--所有的问题都是在人类认识世界和改造世界中产生的
    还原论与what、how、why
    selinux 开启和关闭
    Macbook上打开多个终端的方法
    PHPStorm 快捷键大全(Win/Linux/Mac)
  • 原文地址:https://www.cnblogs.com/liangouyang/p/1260385.html
Copyright © 2011-2022 走看看